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,262 @@
1
+ """Load ``monkeybot_config/monkeybot.yaml`` into ``os.environ`` for unset keys only.
2
+
3
+ Precedence: existing process environment (including ``.env`` via dotenv) wins.
4
+ Discovery: ``MONKEYBOT_CONFIG`` if set, else ``<cwd>/monkeybot_config/monkeybot.yaml``.
5
+ Optional ``includes``: list of paths relative to the primary config file's directory.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+ from collections.abc import Mapping
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ import yaml
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _RUNTIME_ENV_APPLIED = False
21
+
22
+ # (section, key) -> os.environ name
23
+ # YAML gcp/anthropic_vertex project ids map here; prefer GOOGLE_CLOUD_PROJECT when set in .env.
24
+ _GCP_PROJECT_ENV_KEYS = frozenset({"VERTEX_AI_PROJECT_ID", "ANTHROPIC_VERTEX_PROJECT_ID"})
25
+
26
+ # Public alias for CLI / validation (stable API).
27
+ ENV_MAP: dict[tuple[str, str], str] = {
28
+ ("runtime", "log_level"): "LOG_LEVEL",
29
+ ("runtime", "port"): "PORT",
30
+ ("runtime", "gateway_port"): "GATEWAY_PORT",
31
+ ("paths", "agent_md"): "AGENT_MD",
32
+ ("paths", "memory_path"): "MEMORY_PATH",
33
+ ("paths", "memory_storage_uri"): "MEMORY_STORAGE_URI",
34
+ ("paths", "skills_path"): "SKILLS_PATH",
35
+ ("paths", "db_url"): "DB_URL",
36
+ ("paths", "mcp_config"): "MCP_CONFIG",
37
+ ("paths", "command_allowlist_config"): "COMMAND_ALLOWLIST_CONFIG",
38
+ ("paths", "permission_config"): "PERMISSION_CONFIG",
39
+ ("paths", "workspace_root"): "MONKEYBOT_WORKSPACE_ROOT",
40
+ ("model", "provider"): "MODEL_PROVIDER",
41
+ ("model", "name"): "MODEL_NAME",
42
+ ("model", "temperature"): "MODEL_TEMPERATURE",
43
+ ("model", "max_tokens"): "MODEL_MAX_TOKENS",
44
+ ("model", "thinking_budget"): "MODEL_THINKING_BUDGET",
45
+ ("model", "context_window"): "MODEL_CONTEXT_WINDOW",
46
+ ("model", "summarization_model"): "CONTEXT_SUMMARIZATION_MODEL",
47
+ ("model", "max_turns"): "MAX_TURNS",
48
+ ("model", "cache_retention"): "MODEL_CACHE_RETENTION",
49
+ ("gcp", "project_id"): "VERTEX_AI_PROJECT_ID",
50
+ ("gcp", "location"): "VERTEX_AI_LOCATION",
51
+ ("anthropic_vertex", "project_id"): "ANTHROPIC_VERTEX_PROJECT_ID",
52
+ ("anthropic_vertex", "region"): "ANTHROPIC_VERTEX_REGION",
53
+ ("gateway", "pending_response_timeout_sec"): "PENDING_RESPONSE_TIMEOUT_SEC",
54
+ ("gateway", "sse_replay_max"): "SSE_REPLAY_MAX",
55
+ ("gateway", "graceful_shutdown_timeout_sec"): "GRACEFUL_SHUTDOWN_TIMEOUT_SEC",
56
+ ("gateway", "cors_allow_origins"): "MONKEYBOT_CORS_ALLOW_ORIGINS",
57
+ ("context_curation", "enabled"): "CONTEXT_CURATION_ENABLED",
58
+ ("context_curation", "memory_window_lines"): "CONTEXT_CURATION_MEMORY_WINDOW_LINES",
59
+ ("context_curation", "memory_index_cap"): "MEMORY_INDEX_CAP",
60
+ ("context_curation", "memory_token_threshold"): "CONTEXT_CURATION_MEMORY_TOKEN_THRESHOLD",
61
+ ("context_curation", "curator_model"): "CONTEXT_CURATOR_MODEL",
62
+ ("context_curation", "timeout_sec"): "CONTEXT_CURATION_TIMEOUT_SEC",
63
+ ("memory_hook", "enabled"): "MONKEYBOT_MEMORY_HOOK_ENABLED",
64
+ ("subagent", "timeout_sec"): "SUBAGENT_TIMEOUT_SEC",
65
+ ("subagent", "max_turns"): "SUBAGENT_MAX_TURNS",
66
+ ("subagent", "agent_md"): "MONKEYBOT_SUBAGENT_AGENT_MD",
67
+ ("tools", "denied_patterns"): "MONKEYBOT_TOOL_DENIED_PATTERNS",
68
+ ("tools", "read_max_lines"): "MONKEYBOT_READ_MAX_LINES",
69
+ ("tools", "read_default_lines"): "MONKEYBOT_READ_DEFAULT_LINES",
70
+ ("tools", "spill_read_max_lines"): "MONKEYBOT_SPILL_READ_MAX_LINES",
71
+ ("tools", "spill_min_chars"): "MONKEYBOT_SPILL_MIN_CHARS",
72
+ ("tools", "result_budget_fraction"): "MONKEYBOT_RESULT_BUDGET_FRACTION",
73
+ ("tools", "result_budget_floor_tokens"): "MONKEYBOT_RESULT_BUDGET_FLOOR_TOKENS",
74
+ ("compression", "light_ratio"): "MONKEYBOT_PRESSURE_LIGHT_RATIO",
75
+ ("compression", "moderate_ratio"): "MONKEYBOT_PRESSURE_MODERATE_RATIO",
76
+ ("compression", "aggressive_ratio"): "MONKEYBOT_PRESSURE_AGGRESSIVE_RATIO",
77
+ ("compression", "resume_thinking_budget"): "MONKEYBOT_RESUME_THINKING_BUDGET",
78
+ ("web_search", "backend"): "WEB_SEARCH_BACKEND",
79
+ ("web_search", "max_results"): "WEB_SEARCH_MAX_RESULTS",
80
+ ("sandbox", "enabled"): "SANDBOX_ENABLED",
81
+ ("sandbox", "server_url"): "SANDBOX_SERVER_URL",
82
+ ("sandbox", "image"): "SANDBOX_IMAGE",
83
+ ("sandbox", "ttl_seconds"): "SANDBOX_TTL_SECONDS",
84
+ ("fake_provider", "events_json"): "MONKEYBOT_FAKE_PROVIDER_EVENTS",
85
+ ("emission", "style"): "MONKEYBOT_EMISSION_STYLE",
86
+ ("runtime", "transcript_enabled"): "MONKEYBOT_TRANSCRIPT_ENABLED",
87
+ ("runtime", "transcript_include_live"): "MONKEYBOT_TRANSCRIPT_INCLUDE_LIVE",
88
+ ("harness", "mode"): "MONKEYBOT_HARNESS_MODE",
89
+ ("realtime", "websocket.enabled"): "MONKEYBOT_REALTIME_WS_ENABLED",
90
+ ("realtime", "websocket.port"): "MONKEYBOT_REALTIME_WS_PORT",
91
+ ("realtime", "audio.input_format"): "MONKEYBOT_REALTIME_AUDIO_INPUT_FORMAT",
92
+ ("realtime", "audio.output_format"): "MONKEYBOT_REALTIME_AUDIO_OUTPUT_FORMAT",
93
+ ("realtime", "audio.chunk_ms"): "MONKEYBOT_REALTIME_AUDIO_CHUNK_MS",
94
+ ("realtime", "audio.max_utterance_sec"): "MONKEYBOT_REALTIME_AUDIO_MAX_UTTERANCE_SEC",
95
+ ("realtime", "session.max_duration_sec"): "MONKEYBOT_REALTIME_SESSION_MAX_DURATION_SEC",
96
+ ("realtime", "session.idle_timeout_sec"): "MONKEYBOT_REALTIME_SESSION_IDLE_TIMEOUT_SEC",
97
+ ("realtime", "session.max_response_turn_sec"): "MONKEYBOT_REALTIME_SESSION_MAX_RESPONSE_TURN_SEC",
98
+ ("realtime", "session.max_concurrent_sessions"): "MONKEYBOT_REALTIME_SESSION_MAX_CONCURRENT_SESSIONS",
99
+ ("realtime", "metrics.emit_summary_on_close"): "MONKEYBOT_REALTIME_METRICS_EMIT_SUMMARY_ON_CLOSE",
100
+ }
101
+
102
+ # Backward-compatible alias for internal/tests.
103
+ _ENV_MAP = ENV_MAP
104
+
105
+
106
+ def reset_runtime_env_state_for_tests() -> None:
107
+ """Allow a second ``apply_monkeybot_runtime_env`` in the same interpreter (tests only)."""
108
+ global _RUNTIME_ENV_APPLIED
109
+ _RUNTIME_ENV_APPLIED = False
110
+
111
+
112
+ def _deep_merge(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
113
+ out: dict[str, Any] = dict(base)
114
+ for k, v in overlay.items():
115
+ if k in out and isinstance(out[k], dict) and isinstance(v, dict):
116
+ out[k] = _deep_merge(out[k], v)
117
+ else:
118
+ out[k] = v
119
+ return out
120
+
121
+
122
+ def _scalar_to_env(value: Any) -> str | None:
123
+ if value is None:
124
+ return None
125
+ if isinstance(value, bool):
126
+ return "true" if value else "false"
127
+ if isinstance(value, (int, float)):
128
+ return str(value)
129
+ if isinstance(value, str):
130
+ return value
131
+ return None
132
+
133
+
134
+ def _denied_patterns_to_env(value: Any) -> str | None:
135
+ if value is None:
136
+ return None
137
+ if isinstance(value, str):
138
+ return value
139
+ if isinstance(value, list):
140
+ parts = [str(x).strip() for x in value if str(x).strip()]
141
+ return ",".join(parts)
142
+ return None
143
+
144
+
145
+ def _get_nested(mapping: Mapping[str, Any], dotted_key: str) -> Any:
146
+ current: Any = mapping
147
+ for part in dotted_key.split("."):
148
+ if not isinstance(current, dict):
149
+ return None
150
+ current = current.get(part)
151
+ if current is None:
152
+ return None
153
+ return current
154
+
155
+
156
+ def _flatten_config(data: Mapping[str, Any]) -> dict[str, str]:
157
+ out: dict[str, str] = {}
158
+ for (section, key), env_name in ENV_MAP.items():
159
+ sec = data.get(section)
160
+ if not isinstance(sec, dict):
161
+ continue
162
+ raw = _get_nested(sec, key)
163
+ if raw is None:
164
+ continue
165
+ if env_name == "MONKEYBOT_TOOL_DENIED_PATTERNS":
166
+ s = _denied_patterns_to_env(raw)
167
+ else:
168
+ s = _scalar_to_env(raw)
169
+ if s is not None:
170
+ out[env_name] = s
171
+ return out
172
+
173
+
174
+ def _resolve_config_path() -> Path | None:
175
+ explicit = os.environ.get("MONKEYBOT_CONFIG", "").strip()
176
+ if explicit:
177
+ p = Path(explicit).expanduser()
178
+ if p.is_file():
179
+ return p.resolve()
180
+ logger.warning("MONKEYBOT_CONFIG is set but not a file: %s", p)
181
+ return None
182
+ default = Path.cwd() / "monkeybot_config" / "monkeybot.yaml"
183
+ if default.is_file():
184
+ return default.resolve()
185
+ return None
186
+
187
+
188
+ def _load_yaml_file(path: Path) -> dict[str, Any]:
189
+ with open(path, encoding="utf-8") as f:
190
+ loaded = yaml.safe_load(f)
191
+ if loaded is None:
192
+ return {}
193
+ if not isinstance(loaded, dict):
194
+ raise ValueError(f"Expected mapping at root of {path}, got {type(loaded).__name__}")
195
+ return loaded
196
+
197
+
198
+ def _merge_with_includes(primary_path: Path, root: dict[str, Any]) -> dict[str, Any]:
199
+ raw_includes = root.get("includes")
200
+ base = {k: v for k, v in root.items() if k != "includes"}
201
+ if not raw_includes:
202
+ return base
203
+ if not isinstance(raw_includes, list):
204
+ logger.warning("includes: must be a list of paths; ignoring")
205
+ return base
206
+ merged: dict[str, Any] = dict(base)
207
+ parent = primary_path.parent
208
+ for item in raw_includes:
209
+ if not isinstance(item, str) or not item.strip():
210
+ continue
211
+ inc_path = (parent / item.strip()).resolve()
212
+ if not inc_path.is_file():
213
+ logger.warning("include not found, skipping: %s", inc_path)
214
+ continue
215
+ try:
216
+ inc_doc = _load_yaml_file(inc_path)
217
+ except Exception as exc:
218
+ logger.warning("failed to load include %s: %s", inc_path, exc)
219
+ continue
220
+ inc_body = {k: v for k, v in inc_doc.items() if k != "includes"}
221
+ merged = _deep_merge(merged, inc_body)
222
+ return merged
223
+
224
+
225
+ def apply_monkeybot_runtime_env() -> Path | None:
226
+ """Apply YAML-backed defaults to ``os.environ`` (unset keys only). Safe to call twice."""
227
+ global _RUNTIME_ENV_APPLIED
228
+ if _RUNTIME_ENV_APPLIED:
229
+ return None
230
+
231
+ path = _resolve_config_path()
232
+ if path is None:
233
+ _RUNTIME_ENV_APPLIED = True
234
+ logger.debug(
235
+ "No monkeybot.yaml found (MONKEYBOT_CONFIG / monkeybot_config/monkeybot.yaml); skipping"
236
+ )
237
+ return None
238
+
239
+ try:
240
+ root = _load_yaml_file(path)
241
+ merged = _merge_with_includes(path, root)
242
+ flat = _flatten_config(merged)
243
+ except Exception as exc:
244
+ logger.error("Failed to load %s: %s", path, exc)
245
+ raise
246
+
247
+ _RUNTIME_ENV_APPLIED = True
248
+ google_cloud_project = (os.environ.get("GOOGLE_CLOUD_PROJECT") or "").strip()
249
+ for env_key, env_val in flat.items():
250
+ if env_key in os.environ:
251
+ continue
252
+ if env_key in _GCP_PROJECT_ENV_KEYS and google_cloud_project:
253
+ os.environ[env_key] = google_cloud_project
254
+ logger.debug(
255
+ "Set from GOOGLE_CLOUD_PROJECT: %s=%s", env_key, google_cloud_project
256
+ )
257
+ continue
258
+ os.environ[env_key] = env_val
259
+ logger.debug("Set from monkeybot.yaml: %s=%s", env_key, env_val)
260
+
261
+ logger.info("Applied runtime config from %s (%d keys)", path, len(flat))
262
+ return path
@@ -0,0 +1,341 @@
1
+ """Provider resolution and configuration types for the monkeybot harness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from monkeybot.core.config.yaml_loader import load_monkeybot_yaml_dict
11
+ from monkeybot.core.llm.provider import Provider
12
+ from monkeybot.providers.claude import ClaudeProvider
13
+ from monkeybot.providers.gemini import GeminiProvider
14
+ from monkeybot.providers.huggingface import HuggingFaceProvider
15
+ from monkeybot.providers.nvidia import NvidiaProvider
16
+ from monkeybot.providers.ollama import OllamaProvider
17
+ from monkeybot.providers.openai import OpenAIProvider
18
+ from monkeybot.providers.sampling import resolve_model_sampling
19
+ from monkeybot.providers.vertex_claude import VertexClaudeProvider
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _MODEL_PROVIDER_ALIASES: dict[str, str] = {
24
+ "gemini": "google_vertexai",
25
+ "vertex": "google_vertexai",
26
+ "google-vertexai": "google_vertexai",
27
+ "vertex-claude": "vertex_anthropic",
28
+ "vertex_claude": "vertex_anthropic",
29
+ }
30
+
31
+
32
+ def normalize_model_provider(provider: str) -> str:
33
+ """Map config aliases (e.g. ``gemini``, ``vertex-claude``) to canonical provider ids."""
34
+ key = provider.strip().lower()
35
+ return _MODEL_PROVIDER_ALIASES.get(key, key)
36
+
37
+
38
+ class ConfigError(Exception):
39
+ """Configuration error with actionable message."""
40
+
41
+ pass
42
+
43
+
44
+ @dataclass
45
+ class CustomMemoryFolder:
46
+ """User-defined memory folder registered with the memory organizer classifier."""
47
+
48
+ name: str
49
+ description: str
50
+
51
+ def __post_init__(self) -> None:
52
+ import re
53
+
54
+ if not re.match(r"^[a-z0-9-]+$", self.name):
55
+ raise ConfigError(
56
+ f"CustomMemoryFolder.name '{self.name}' is invalid. "
57
+ "Use lowercase letters, digits, and hyphens only."
58
+ )
59
+ reserved = {"episodic", "semantic", "procedural", "working", "raw"}
60
+ if self.name in reserved:
61
+ raise ConfigError(
62
+ f"CustomMemoryFolder.name '{self.name}' conflicts with a built-in folder. "
63
+ f"Reserved names: {sorted(reserved)}"
64
+ )
65
+
66
+
67
+ @dataclass
68
+ class SubagentConfig:
69
+ """Configuration for a single named subagent persona."""
70
+
71
+ name: str
72
+ description: str
73
+ skills: list[str]
74
+ agent_md: str | None = None
75
+ model: str | None = None
76
+ vertex_location: str | None = None
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class ProviderConfig:
81
+ """Native streaming Provider plus model id."""
82
+
83
+ provider: Provider
84
+ model: str
85
+
86
+
87
+ def _resolve_gcp_project_id() -> str:
88
+ """GCP project for Vertex providers."""
89
+ return (
90
+ (os.getenv("GCP_PROJECT_ID") or "").strip()
91
+ or (os.getenv("VERTEX_AI_PROJECT_ID") or "").strip()
92
+ or (os.getenv("ANTHROPIC_VERTEX_PROJECT_ID") or "").strip()
93
+ or (os.getenv("GOOGLE_CLOUD_PROJECT") or "").strip()
94
+ )
95
+
96
+
97
+ def get_provider_config(
98
+ provider: str | None = None,
99
+ model_name: str | None = None,
100
+ temperature: float | None = None,
101
+ max_tokens: int | None = None,
102
+ thinking_budget: int | None = None,
103
+ ) -> ProviderConfig:
104
+ """Resolve a Provider and model id from environment or explicit parameters."""
105
+ raw_provider = str(provider or os.getenv("MODEL_PROVIDER") or "google_vertexai")
106
+ provider_key = normalize_model_provider(raw_provider)
107
+ if provider_key == "fake":
108
+ raise ValueError(
109
+ "MODEL_PROVIDER=fake is for gateway/tests only; inject ScriptedFakeProvider directly "
110
+ "or use the gateway fake provider path."
111
+ )
112
+ resolved_model = str(model_name or os.getenv("MODEL_NAME") or "gemini-2.5-flash")
113
+ sampling = resolve_model_sampling(temperature=temperature, max_tokens=max_tokens)
114
+ thinking_budget = (
115
+ thinking_budget if thinking_budget is not None else int(os.getenv("MODEL_THINKING_BUDGET", "-1"))
116
+ )
117
+ if provider_key == "google_vertexai":
118
+ return ProviderConfig(
119
+ GeminiProvider(
120
+ temperature=sampling.temperature,
121
+ max_tokens=sampling.max_tokens,
122
+ thinking_budget=thinking_budget,
123
+ ),
124
+ resolved_model,
125
+ )
126
+ if provider_key == "google_genai":
127
+ api_key = (os.getenv("GEMINI_API_KEY") or "").strip()
128
+ if not api_key:
129
+ raise ValueError(
130
+ "MODEL_PROVIDER=google_genai requires GEMINI_API_KEY. "
131
+ "Set it in your environment or .env file."
132
+ )
133
+ return ProviderConfig(
134
+ GeminiProvider(
135
+ temperature=sampling.temperature,
136
+ max_tokens=sampling.max_tokens,
137
+ thinking_budget=thinking_budget,
138
+ api_key=api_key,
139
+ ),
140
+ resolved_model,
141
+ )
142
+ if provider_key == "openai":
143
+ return ProviderConfig(
144
+ OpenAIProvider(
145
+ temperature=sampling.temperature,
146
+ max_tokens=sampling.max_tokens,
147
+ ),
148
+ resolved_model,
149
+ )
150
+ if provider_key == "anthropic":
151
+ return ProviderConfig(
152
+ ClaudeProvider(
153
+ temperature=sampling.temperature,
154
+ max_tokens=sampling.max_tokens,
155
+ ),
156
+ resolved_model,
157
+ )
158
+ if provider_key == "vertex_anthropic":
159
+ project = _resolve_gcp_project_id()
160
+ if not project:
161
+ raise ValueError(
162
+ "vertex_anthropic provider requires a GCP project. "
163
+ "Set GCP_PROJECT_ID, VERTEX_AI_PROJECT_ID, ANTHROPIC_VERTEX_PROJECT_ID, "
164
+ "or GOOGLE_CLOUD_PROJECT (or gcp.project_id in monkeybot.yaml)."
165
+ )
166
+ if os.getenv("VERTEX_AI_LOCATION"):
167
+ logger.warning(
168
+ "VERTEX_AI_LOCATION is no longer read for vertex_anthropic; "
169
+ "set ANTHROPIC_VERTEX_REGION instead"
170
+ )
171
+ region = (os.getenv("ANTHROPIC_VERTEX_REGION") or "us-east5").strip() or "us-east5"
172
+ return ProviderConfig(
173
+ VertexClaudeProvider(
174
+ project_id=project,
175
+ region=region,
176
+ temperature=sampling.temperature,
177
+ max_tokens=sampling.max_tokens,
178
+ ),
179
+ resolved_model,
180
+ )
181
+ if provider_key == "huggingface":
182
+ return ProviderConfig(
183
+ HuggingFaceProvider(
184
+ temperature=sampling.temperature,
185
+ max_tokens=sampling.max_tokens,
186
+ ),
187
+ resolved_model,
188
+ )
189
+ if provider_key == "ollama":
190
+ return ProviderConfig(
191
+ OllamaProvider(
192
+ temperature=sampling.temperature,
193
+ max_tokens=sampling.max_tokens,
194
+ thinking_budget=thinking_budget,
195
+ ),
196
+ resolved_model,
197
+ )
198
+ if provider_key == "nvidia":
199
+ return ProviderConfig(
200
+ NvidiaProvider(
201
+ temperature=sampling.temperature,
202
+ max_tokens=sampling.max_tokens,
203
+ ),
204
+ resolved_model,
205
+ )
206
+ if provider_key == "aws_bedrock":
207
+ from monkeybot.providers.bedrock import BedrockClaudeProvider # noqa: PLC0415
208
+
209
+ aws_region = os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION")
210
+ return ProviderConfig(
211
+ BedrockClaudeProvider(
212
+ aws_region=aws_region,
213
+ temperature=sampling.temperature,
214
+ max_tokens=sampling.max_tokens,
215
+ ),
216
+ resolved_model,
217
+ )
218
+ raise ValueError(
219
+ f"Unsupported model provider: {provider_key}. "
220
+ "Supported providers: google_vertexai, openai, anthropic, vertex_anthropic, "
221
+ "huggingface, ollama, nvidia, aws_bedrock"
222
+ )
223
+
224
+
225
+ def _parse_subagent_entries(raw_entries: Any) -> list[SubagentConfig]:
226
+ if not raw_entries or not isinstance(raw_entries, list):
227
+ return []
228
+ configs: list[SubagentConfig] = []
229
+ for entry in raw_entries:
230
+ if not isinstance(entry, dict):
231
+ logger.warning("Skipping non-dict subagent entry: %s", entry)
232
+ continue
233
+ if "name" not in entry or "description" not in entry:
234
+ logger.warning("Skipping subagent entry missing 'name' or 'description': %s", entry)
235
+ continue
236
+ vloc = entry.get("vertex_location")
237
+ if vloc is None and "location" in entry:
238
+ vloc = entry.get("location")
239
+ raw_skills = entry.get("skills", [])
240
+ skills = list(raw_skills) if isinstance(raw_skills, list) else []
241
+ agent_md = entry.get("agent_md") or entry.get("prompt_file")
242
+ if agent_md is not None and not isinstance(agent_md, str):
243
+ logger.warning("Skipping subagent %s: agent_md must be a string", entry.get("name"))
244
+ continue
245
+ configs.append(
246
+ SubagentConfig(
247
+ name=entry["name"],
248
+ description=entry["description"],
249
+ skills=skills,
250
+ agent_md=agent_md.strip() if isinstance(agent_md, str) and agent_md.strip() else None,
251
+ model=entry.get("model"),
252
+ vertex_location=vloc,
253
+ )
254
+ )
255
+ return configs
256
+
257
+
258
+ def get_subagent_configs(config_path: str | None = None) -> list[SubagentConfig]:
259
+ """Return subagent configurations from ``subagents:`` in monkeybot.yaml."""
260
+ _, doc = load_monkeybot_yaml_dict(config_path)
261
+ return _parse_subagent_entries(doc.get("subagents"))
262
+
263
+
264
+ def get_subagent_registry(config_path: str | None = None) -> dict[str, SubagentConfig]:
265
+ """Named subagent personas keyed by ``name``; raises :class:`ConfigError` on duplicates."""
266
+ registry: dict[str, SubagentConfig] = {}
267
+ for cfg in get_subagent_configs(config_path):
268
+ if cfg.name in registry:
269
+ raise ConfigError(f"Duplicate subagent name in monkeybot.yaml: {cfg.name!r}")
270
+ registry[cfg.name] = cfg
271
+ return registry
272
+
273
+
274
+ def _bool_config_flag(
275
+ doc: dict[str, Any],
276
+ section: str,
277
+ key: str,
278
+ *,
279
+ default: bool,
280
+ label: str,
281
+ ) -> bool:
282
+ section_obj = doc.get(section)
283
+ if not isinstance(section_obj, dict):
284
+ return default
285
+ raw = section_obj.get(key)
286
+ if raw is None:
287
+ return default
288
+ if isinstance(raw, bool):
289
+ return raw
290
+ raise ConfigError(f"{label} must be true or false, got {raw!r}")
291
+
292
+
293
+ def auto_schema_enabled_from_config(config_path: str | None = None) -> bool:
294
+ """Whether storage backends should apply DDL on ``open()`` (``paths.auto_schema``).
295
+
296
+ Defaults to ``True`` when the key is absent. Only read from monkeybot.yaml —
297
+ not from environment variables.
298
+ """
299
+ _, doc = load_monkeybot_yaml_dict(config_path)
300
+ paths = doc.get("paths")
301
+ if not isinstance(paths, dict):
302
+ return True
303
+ raw = paths.get("auto_schema")
304
+ if raw is None:
305
+ return True
306
+ if isinstance(raw, bool):
307
+ return raw
308
+ raise ConfigError(f"paths.auto_schema must be true or false, got {raw!r}")
309
+
310
+
311
+ def vertex_google_search_enabled_from_config(config_path: str | None = None) -> bool:
312
+ """Whether the main agent enables Gemini's native ``google_search`` grounding tool.
313
+
314
+ Read from ``web_search.vertex_google_search`` in monkeybot.yaml. Additive to, and
315
+ independent of, ``web_search.backend`` (the harness's pluggable DuckDuckGo/Tavily/
316
+ Firecrawl custom tool). Defaults to ``False`` when absent. Config-file only.
317
+ """
318
+ _, doc = load_monkeybot_yaml_dict(config_path)
319
+ return _bool_config_flag(
320
+ doc,
321
+ "web_search",
322
+ "vertex_google_search",
323
+ default=False,
324
+ label="web_search.vertex_google_search",
325
+ )
326
+
327
+
328
+ def subagent_vertex_google_search_from_config(config_path: str | None = None) -> bool:
329
+ """Whether subagent runs enable Gemini's native ``google_search`` grounding tool.
330
+
331
+ Read from ``subagent.vertex_google_search`` in monkeybot.yaml. Defaults to ``False``
332
+ when absent. Config-file only — not exposed via environment variables.
333
+ """
334
+ _, doc = load_monkeybot_yaml_dict(config_path)
335
+ return _bool_config_flag(
336
+ doc,
337
+ "subagent",
338
+ "vertex_google_search",
339
+ default=False,
340
+ label="subagent.vertex_google_search",
341
+ )