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,441 @@
1
+ """Subprocess worker for the ``task`` tool: stdin :class:`SubagentEnvelope` JSON, stdout NDJSON events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ import sys
9
+ import uuid
10
+ from collections.abc import AsyncIterator
11
+ from pathlib import Path
12
+ from typing import Any, cast
13
+
14
+ from dotenv import load_dotenv
15
+
16
+ from monkeybot.core.config.settings import (
17
+ auto_schema_enabled_from_config,
18
+ get_provider_config,
19
+ normalize_model_provider,
20
+ subagent_vertex_google_search_from_config,
21
+ )
22
+ from monkeybot.core.context import TurnContext, build_context
23
+ from monkeybot.core.llm.provider import (
24
+ Done,
25
+ Provider,
26
+ ProviderEvent,
27
+ TextDelta,
28
+ ToolCall,
29
+ UsageEvent,
30
+ )
31
+ from monkeybot.core.mcp.mcp_client import MCPClient
32
+ from monkeybot.core.memory.subsystem import MemorySubsystem
33
+ from monkeybot.core.persistence.backends import HistoryStore, create_storage_backend
34
+ from monkeybot.core.runtime.events import AgentEvent, Error, event_to_json
35
+ from monkeybot.core.runtime.loop import run as run_loop
36
+ from monkeybot.core.subagents.subagent_proto import (
37
+ SubagentEnvelope,
38
+ normalize_sqlite_db_url,
39
+ resolve_agent_project_root,
40
+ resolve_project_path,
41
+ resolve_subagent_agent_md_path,
42
+ )
43
+ from monkeybot.core.testing.mocks_provider import ScriptedFakeProvider
44
+ from monkeybot.core.tools.core_tool_executor import CoreToolExecutor
45
+ from monkeybot.core.tools.inspector import CommandTierInspector, RulesInspector, ToolInspector
46
+ from monkeybot.core.tools.permission import try_load_permission_inspector
47
+ from monkeybot.core.workspace import create_workspace_storage
48
+ from monkeybot.web_search import WebSearchTool
49
+ from monkeybot.web_search import build_backend as _build_web_search_backend
50
+
51
+ _BUILTIN_RUN_LOOP = run_loop
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+
56
+ def _subagent_memory_uri(envelope: SubagentEnvelope) -> str:
57
+ """Envelope URI is authoritative; empty means no memory (ignore ambient env)."""
58
+ return envelope.memory_storage_uri.strip()
59
+
60
+
61
+ def init_observability() -> bool:
62
+ from monkeybot.observability import init_observability as _init
63
+
64
+ return _init()
65
+
66
+
67
+ def shutdown_observability() -> None:
68
+ from monkeybot.observability import shutdown_observability as _shutdown
69
+
70
+ _shutdown()
71
+
72
+
73
+ def _attach_trace_from_envelope(envelope: SubagentEnvelope) -> object | None:
74
+ if not envelope.traceparent:
75
+ return None
76
+ try:
77
+ from opentelemetry import context as otel_context
78
+
79
+ from monkeybot.observability.propagation import extract_traceparent
80
+ except ImportError:
81
+ return None
82
+ ctx = extract_traceparent({"traceparent": envelope.traceparent})
83
+ if ctx is None:
84
+ return None
85
+ return otel_context.attach(ctx)
86
+
87
+
88
+ def _detach_trace(token: object | None) -> None:
89
+ if token is None:
90
+ return
91
+ try:
92
+ from opentelemetry import context as otel_context
93
+
94
+ otel_context.detach(cast(Any, token))
95
+ except Exception:
96
+ logger.debug("trace detach failed", exc_info=True)
97
+ return
98
+
99
+
100
+ def _reset_trace_context_if_unattached(attach_token: object | None) -> object | None:
101
+ if attach_token is not None:
102
+ return None
103
+ try:
104
+ from opentelemetry import context as otel_context
105
+
106
+ return otel_context.attach(otel_context.Context())
107
+ except ImportError:
108
+ return None
109
+
110
+
111
+ def _clear_span_exporter_buffer() -> None:
112
+ try:
113
+ from opentelemetry import trace
114
+
115
+ provider = trace.get_tracer_provider()
116
+ multi = getattr(provider, "_active_span_processor", None)
117
+ processors = getattr(multi, "_span_processors", ()) if multi is not None else ()
118
+ for proc in processors:
119
+ exporter = getattr(proc, "span_exporter", None)
120
+ if exporter is not None and hasattr(exporter, "clear"):
121
+ exporter.clear()
122
+ except Exception:
123
+ logger.debug("span exporter buffer clear failed", exc_info=True)
124
+ return
125
+
126
+
127
+ async def _stream_run_loop_events(
128
+ body: str,
129
+ ctx: TurnContext,
130
+ *,
131
+ provider: Provider,
132
+ history: HistoryStore,
133
+ inspectors: list[ToolInspector],
134
+ tool_executor: CoreToolExecutor,
135
+ run_id: str,
136
+ max_turns: int,
137
+ vertex_google_search: bool = False,
138
+ ) -> AsyncIterator[AgentEvent]:
139
+ if run_loop is _BUILTIN_RUN_LOOP:
140
+ async for evt in run_loop(
141
+ body,
142
+ ctx,
143
+ provider=provider,
144
+ history=history,
145
+ inspectors=inspectors,
146
+ tool_executor=tool_executor,
147
+ cancelled=None,
148
+ max_turns=max_turns,
149
+ vertex_google_search=vertex_google_search,
150
+ ):
151
+ yield evt
152
+ return
153
+
154
+ from monkeybot.observability.spans import span_run
155
+
156
+ async with span_run(ctx, user_message=body):
157
+ async for evt in run_loop(
158
+ body,
159
+ ctx,
160
+ provider=provider,
161
+ history=history,
162
+ inspectors=inspectors,
163
+ tool_executor=tool_executor,
164
+ cancelled=None,
165
+ max_turns=max_turns,
166
+ vertex_google_search=vertex_google_search,
167
+ ):
168
+ yield evt
169
+
170
+
171
+ def _resolve_provider() -> Provider:
172
+ mode = normalize_model_provider(os.environ.get("MODEL_PROVIDER", "google_vertexai"))
173
+ if mode != "fake":
174
+ return get_provider_config(provider=mode).provider
175
+
176
+ import json
177
+
178
+ raw = os.environ.get("MONKEYBOT_FAKE_PROVIDER_EVENTS", "")
179
+ if not raw:
180
+ return ScriptedFakeProvider(
181
+ [
182
+ TextDelta(
183
+ text="subagent fake provider: set MONKEYBOT_FAKE_PROVIDER_EVENTS for scripted tools."
184
+ ),
185
+ UsageEvent(input_tokens=1, output_tokens=1, cached_tokens=0),
186
+ Done(),
187
+ ]
188
+ )
189
+
190
+ decoded = json.loads(raw)
191
+ turns: list[list[ProviderEvent]] = []
192
+ for turn in decoded:
193
+ events: list[ProviderEvent] = []
194
+ if not isinstance(turn, list):
195
+ continue
196
+ for item in turn:
197
+ if not isinstance(item, dict):
198
+ continue
199
+ k = item.get("kind")
200
+ if k == "text_delta":
201
+ events.append(TextDelta(text=str(item.get("text", ""))))
202
+ elif k == "usage":
203
+ events.append(
204
+ UsageEvent(
205
+ input_tokens=int(item.get("input_tokens", 0)),
206
+ output_tokens=int(item.get("output_tokens", 0)),
207
+ cached_tokens=int(item.get("cached_tokens", 0)),
208
+ )
209
+ )
210
+ elif k == "tool_call":
211
+ events.append(
212
+ ToolCall(
213
+ call_id=str(item["call_id"]),
214
+ name=str(item["name"]),
215
+ args=dict(item.get("args", {})),
216
+ )
217
+ )
218
+ elif k == "done":
219
+ events.append(Done())
220
+ if events:
221
+ turns.append(events)
222
+ if not turns:
223
+ turns = [[TextDelta(text="hello"), Done()]]
224
+ flat: list[ProviderEvent] = [ev for turn in turns for ev in turn]
225
+ return ScriptedFakeProvider(flat)
226
+
227
+
228
+ async def _async_main() -> None:
229
+ raw = sys.stdin.read()
230
+ if not raw.strip():
231
+ print(event_to_json(Error(request_id="", error="subagent_worker: empty stdin")), flush=True)
232
+ raise SystemExit(1)
233
+
234
+ envelope = SubagentEnvelope.from_json(raw)
235
+ attach_token: object | None = _attach_trace_from_envelope(envelope)
236
+ reset_token: object | None = None
237
+
238
+ agent_root = resolve_agent_project_root()
239
+ dotenv_path = agent_root / ".env"
240
+ if dotenv_path.is_file():
241
+ load_dotenv(dotenv_path)
242
+
243
+ ws = Path(os.environ["MONKEYBOT_SUBAGENT_WORKSPACE"]).resolve()
244
+ os.chdir(ws)
245
+
246
+ mem_uri = _subagent_memory_uri(envelope)
247
+
248
+ skills = Path(os.environ["MONKEYBOT_SUBAGENT_SKILLS_PATH"]).resolve()
249
+
250
+ if envelope.agent_md:
251
+ agent_md_path = Path(envelope.agent_md).expanduser().resolve()
252
+ if not agent_md_path.is_file():
253
+ agent_md_path = resolve_project_path(envelope.agent_md, agent_root)
254
+ else:
255
+ agent_md_path = resolve_subagent_agent_md_path(agent_root) or resolve_project_path(
256
+ "AGENT.md", agent_root
257
+ )
258
+
259
+ db_url = normalize_sqlite_db_url(
260
+ os.environ.get("DB_URL", "sqlite:///data/monkeybot.db"), agent_root
261
+ )
262
+ backend = create_storage_backend(db_url)
263
+ mcp: MCPClient | None = None
264
+ executor: CoreToolExecutor | None = None
265
+
266
+ try:
267
+ await backend.open(run_schema=auto_schema_enabled_from_config())
268
+
269
+ mcp = MCPClient()
270
+ mcp_config = resolve_project_path(
271
+ os.environ.get("MCP_CONFIG", "monkeybot_config/mcp.json"), agent_root
272
+ )
273
+ strict = os.environ.get("MCP_STRICT_LOAD", "").strip().lower() in ("1", "true", "yes")
274
+ try:
275
+ await mcp.load_from_config(mcp_config, raise_on_error=strict)
276
+ except OSError as exc:
277
+ logger.info("MCP config skipped (%s): %s", mcp_config, exc)
278
+
279
+ inspectors: list[ToolInspector] = []
280
+ run_allow_cmds: list[str] | None = None
281
+ run_allow_paths: list[str] | None = None
282
+ tiers_path = resolve_project_path(
283
+ os.environ.get("COMMAND_ALLOWLIST_CONFIG", "monkeybot_config/command_allowlist.yaml"),
284
+ agent_root,
285
+ )
286
+ try:
287
+ tier_insp = CommandTierInspector(tiers_path)
288
+ inspectors.append(tier_insp)
289
+ run_allow_cmds = list(tier_insp.allowed_commands)
290
+ run_allow_paths = list(tier_insp.allowed_path_prefixes)
291
+ except FileNotFoundError:
292
+ logger.info("command tiers missing (%s); allowing all tool calls", tiers_path)
293
+ except Exception:
294
+ logger.exception("command tier load failed")
295
+
296
+ raw_rules = os.environ.get("MONKEYBOT_TOOL_DENIED_PATTERNS")
297
+ if raw_rules is None:
298
+ rules_patterns = ["rm -rf", "/etc/passwd", "DROP TABLE"]
299
+ else:
300
+ rules_patterns = [p.strip() for p in raw_rules.split(",") if p.strip()]
301
+ if rules_patterns:
302
+ inspectors.append(RulesInspector(rules_patterns))
303
+
304
+ perm_path = resolve_project_path(
305
+ os.environ.get("PERMISSION_CONFIG", "monkeybot_config/permissions.yaml"),
306
+ agent_root,
307
+ )
308
+ # Subagents have no interactive session to prompt: reuse the parent's
309
+ # ruleset, but resolve "ask" to deny instead of a confirm that can
310
+ # never be answered.
311
+ perm_insp = try_load_permission_inspector(perm_path, allow_ask=False)
312
+ if perm_insp is not None:
313
+ inspectors.append(perm_insp)
314
+
315
+ provider = _resolve_provider()
316
+ thread_id = f"subagent:{envelope.parent_run_id}:{uuid.uuid4().hex[:10]}"
317
+ request_id = f"sub-{uuid.uuid4().hex[:12]}"
318
+
319
+ cap_raw = os.environ.get("MODEL_CONTEXT_WINDOW", "200000").strip()
320
+ try:
321
+ context_window_tokens = max(1, int(cap_raw))
322
+ except ValueError:
323
+ context_window_tokens = 200_000
324
+
325
+ try:
326
+ _ws_backend = _build_web_search_backend()
327
+ _ws_tool: WebSearchTool | None = (
328
+ WebSearchTool(_ws_backend) if _ws_backend is not None else None
329
+ )
330
+ except Exception:
331
+ logger.debug("web search backend init failed", exc_info=True)
332
+ _ws_tool = None
333
+
334
+ extra_tools = [_ws_tool] if _ws_tool is not None else []
335
+
336
+ memory: MemorySubsystem | None = None
337
+ if mem_uri:
338
+ storage = create_workspace_storage(mem_uri)
339
+ memory = MemorySubsystem(
340
+ storage=storage,
341
+ provider=provider,
342
+ model=envelope.model,
343
+ memory_uri=mem_uri,
344
+ )
345
+
346
+ ctx = await build_context(
347
+ thread_id,
348
+ request_id,
349
+ agent_md_path=agent_md_path,
350
+ memory=memory,
351
+ skills_path=skills,
352
+ mcp_client=mcp,
353
+ parent_run_id=envelope.parent_run_id,
354
+ model=envelope.model,
355
+ include_task_tool=False,
356
+ workspace_root=ws,
357
+ context_window_tokens=context_window_tokens,
358
+ enable_context_curation=False,
359
+ extra_tools=extra_tools,
360
+ )
361
+
362
+ executor = CoreToolExecutor(
363
+ workspace_root=ws,
364
+ memory=memory,
365
+ skills_path=skills,
366
+ mcp=mcp,
367
+ extra_tools=extra_tools,
368
+ run_command_allowed_commands=run_allow_cmds,
369
+ run_command_allowed_path_prefixes=run_allow_paths,
370
+ )
371
+ history = backend.history()
372
+
373
+ body = envelope.task.strip()
374
+ if envelope.context.strip():
375
+ body += "\n\n---\nContext from parent agent:\n" + envelope.context.strip()
376
+
377
+ max_turns_raw = os.environ.get("SUBAGENT_MAX_TURNS", "").strip()
378
+ if max_turns_raw:
379
+ max_turns = max(1, int(max_turns_raw))
380
+ else:
381
+ max_turns = max(1, int(os.environ.get("MAX_TURNS", "25")))
382
+
383
+ from monkeybot.observability.spans import span_subagent
384
+
385
+ reset_token = _reset_trace_context_if_unattached(attach_token)
386
+ _clear_span_exporter_buffer()
387
+ init_observability()
388
+ try:
389
+ # Subagents read memory (index, search_memory) via MemorySubsystem but do not
390
+ # register memory hooks — chat_log / raw / organizer writes stay on the parent
391
+ # gateway to avoid duplicate or conflicting durable memory updates.
392
+ async with span_subagent(
393
+ thread_id=thread_id,
394
+ request_id=request_id,
395
+ parent_run_id=envelope.parent_run_id,
396
+ subagent_type=envelope.subagent_type,
397
+ agent_md=envelope.agent_md,
398
+ ):
399
+ async for evt in _stream_run_loop_events(
400
+ body,
401
+ ctx,
402
+ provider=provider,
403
+ history=history,
404
+ inspectors=inspectors,
405
+ tool_executor=executor,
406
+ run_id=request_id,
407
+ max_turns=max_turns,
408
+ vertex_google_search=subagent_vertex_google_search_from_config(),
409
+ ):
410
+ print(event_to_json(evt), flush=True)
411
+ finally:
412
+ shutdown_observability()
413
+ finally:
414
+ _detach_trace(reset_token)
415
+ _detach_trace(attach_token)
416
+ if executor is not None:
417
+ await executor.aclose()
418
+ if mcp is not None:
419
+ for name in list(getattr(mcp, "_servers", {}).keys()):
420
+ await mcp.disconnect(name)
421
+ await backend.close()
422
+
423
+
424
+ def main() -> None:
425
+ from monkeybot.core.logging_utils import normalize_log_level
426
+
427
+ logging.basicConfig(level=normalize_log_level(os.environ.get("LOG_LEVEL"), default="WARNING"))
428
+ try:
429
+ asyncio.run(_async_main())
430
+ except SystemExit:
431
+ raise
432
+ except KeyboardInterrupt:
433
+ raise
434
+ except Exception as exc:
435
+ logger.exception("subagent_worker fatal")
436
+ print(event_to_json(Error(request_id="", error=str(exc))), flush=True)
437
+ raise SystemExit(1) from exc
438
+
439
+
440
+ if __name__ == "__main__":
441
+ main()