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,72 @@
1
+ """Frozen attachment descriptor text render/parse."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ParsedAttachmentDescriptor:
11
+ attachment_id: str
12
+ filename: str
13
+ mime_type: str
14
+ description: str
15
+
16
+
17
+ _ATTACHMENT_LINE_RE = re.compile(
18
+ r'^\[attachment\s+(?P<id>\S+)\s+(?P<filename>.+?)\s+\((?P<mime>[^)]+)\):\s+'
19
+ r'(?P<desc>.+?)\.\s+Call read_attachment\("(?P=id)"\)\s+to view pixels again\.\]$'
20
+ )
21
+
22
+
23
+ def render_attachment_descriptor_text(
24
+ *,
25
+ attachment_id: str,
26
+ filename: str,
27
+ mime_type: str,
28
+ description: str,
29
+ ) -> str:
30
+ desc = description.strip() or f"{filename} ({mime_type}) attached by user."
31
+ if len(desc) > 500:
32
+ desc = desc[:497].rstrip() + "..."
33
+ return (
34
+ f'[attachment {attachment_id} {filename} ({mime_type}): {desc}. '
35
+ f'Call read_attachment("{attachment_id}") to view pixels again.]'
36
+ )
37
+
38
+
39
+ def parse_attachment_descriptor_text(text: str) -> ParsedAttachmentDescriptor | None:
40
+ m = _ATTACHMENT_LINE_RE.match(text.strip())
41
+ if m is None:
42
+ return None
43
+ return ParsedAttachmentDescriptor(
44
+ attachment_id=m.group("id"),
45
+ filename=m.group("filename"),
46
+ mime_type=m.group("mime"),
47
+ description=m.group("desc"),
48
+ )
49
+
50
+
51
+ def render_tool_media_freeze_text(
52
+ *,
53
+ tool_name: str,
54
+ attachment_id: str | None,
55
+ kind: str,
56
+ ) -> str:
57
+ if attachment_id:
58
+ return (
59
+ f"[{tool_name} {attachment_id} result: {kind} shown earlier; "
60
+ f'call read_attachment("{attachment_id}") to reload.]'
61
+ )
62
+ return f"[{tool_name} result: {kind} shown earlier; call tool again to reload.]"
63
+
64
+
65
+ def filename_from_metadata(metadata: dict[str, object] | None, *, fallback: str) -> str:
66
+ if not metadata:
67
+ return fallback
68
+ for key in ("filename", "name"):
69
+ val = metadata.get(key)
70
+ if isinstance(val, str) and val.strip():
71
+ return val.strip()
72
+ return fallback
@@ -0,0 +1,54 @@
1
+ """Attachment-related tool definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from monkeybot.core.types.types_tools import ToolDef
6
+
7
+ READ_ATTACHMENT_SCHEMA: dict[str, object] = {
8
+ "type": "object",
9
+ "properties": {
10
+ "attachment_id": {
11
+ "type": "string",
12
+ "description": "Session attachment id from ## Session attachments.",
13
+ },
14
+ },
15
+ "required": ["attachment_id"],
16
+ }
17
+
18
+ RENDER_IMAGE_SCHEMA: dict[str, object] = {
19
+ "type": "object",
20
+ "properties": {
21
+ "path": {
22
+ "type": "string",
23
+ "description": "Workspace-relative path to a PNG/JPEG/GIF/WebP image file.",
24
+ },
25
+ "caption": {
26
+ "type": "string",
27
+ "description": "Optional short caption shown with the image in chat.",
28
+ },
29
+ },
30
+ "required": ["path"],
31
+ }
32
+
33
+
34
+ def read_attachment_tool_def() -> ToolDef:
35
+ return ToolDef(
36
+ "read_attachment",
37
+ (
38
+ "Load a session attachment by id for visual or PDF analysis. "
39
+ "Ids are listed under ## Session attachments when present."
40
+ ),
41
+ READ_ATTACHMENT_SCHEMA,
42
+ parallel_safe=True,
43
+ )
44
+
45
+
46
+ def render_image_tool_def() -> ToolDef:
47
+ return ToolDef(
48
+ "render_image",
49
+ (
50
+ "Display a workspace image inline in the chat UI. "
51
+ "Use after skill scripts write PNG/JPEG/GIF/WebP under ./generated-media/ or similar."
52
+ ),
53
+ RENDER_IMAGE_SCHEMA,
54
+ )
@@ -0,0 +1,242 @@
1
+ """Optional cold-start wiring for harness-as-library (Pattern B / C).
2
+
3
+ Callers who prefer explicit construction can ignore this module and wire
4
+ :class:`~monkeybot.core.persistence.backends.StorageBackend`,
5
+ :class:`~monkeybot.core.memory.subsystem.MemorySubsystem`, :class:`~monkeybot.core.mcp.mcp_client.MCPClient`,
6
+ and a :class:`~monkeybot.core.llm.provider.Provider` themselves.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ from monkeybot.core.config.settings import auto_schema_enabled_from_config, get_provider_config
17
+ from monkeybot.core.context import build_context
18
+ from monkeybot.core.hooks import HookManager
19
+ from monkeybot.core.llm.provider import Provider
20
+ from monkeybot.core.llm.usage import usage_from_totals
21
+ from monkeybot.core.mcp.mcp_client import MCPClient
22
+ from monkeybot.core.memory.subsystem import MemorySubsystem
23
+ from monkeybot.core.persistence.backends import StorageBackend, create_storage_backend
24
+ from monkeybot.core.runtime.events import AssistantDelta, Error, TurnComplete
25
+ from monkeybot.core.runtime.loop import run as run_loop
26
+ from monkeybot.core.tools.core_tool_executor import CoreToolExecutor
27
+ from monkeybot.core.tools.inspector import load_command_tier_policy
28
+ from monkeybot.core.workspace import create_workspace_storage
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class PatternBcTurnError(RuntimeError):
34
+ """Raised when :func:`run_pattern_bc_turn` receives an :class:`~monkeybot.core.runtime.events.Error` event."""
35
+
36
+ def __init__(self, message: str, *, request_id: str = "") -> None:
37
+ super().__init__(message)
38
+ self.request_id = request_id
39
+
40
+
41
+ @dataclass
42
+ class HarnessDeps:
43
+ """Process-scoped dependencies for ``build_context`` + ``run_loop``."""
44
+
45
+ storage: StorageBackend
46
+ memory: MemorySubsystem | None
47
+ mcp: MCPClient
48
+ provider: Provider
49
+ model: str
50
+
51
+ async def close(self) -> None:
52
+ """Close persisted storage and tear down MCP sessions.
53
+
54
+ :class:`~monkeybot.core.workspace.protocol.WorkspaceStorage` (behind ``memory``)
55
+ does not define ``aclose``/``close`` today; cloud SDK clients are process-scoped.
56
+ Call explicit workspace teardown here if the protocol gains lifecycle hooks.
57
+ """
58
+ await self.storage.close()
59
+ await self.mcp.disconnect_all()
60
+
61
+
62
+ def _resolve_run_command_allowlists() -> tuple[list[str] | None, list[str] | None]:
63
+ """Load allowlists from ``COMMAND_ALLOWLIST_CONFIG`` when present; else executor defaults."""
64
+ path_str = os.environ.get("COMMAND_ALLOWLIST_CONFIG", "").strip()
65
+ if not path_str:
66
+ return None, None
67
+ path = Path(path_str)
68
+ if not path.is_file():
69
+ logger.info("command allowlist missing (%s); using CoreToolExecutor defaults", path)
70
+ return None, None
71
+ try:
72
+ policy = load_command_tier_policy(path)
73
+ return list(policy.allowed_commands), list(policy.allowed_path_prefixes)
74
+ except Exception as exc:
75
+ logger.warning("command allowlist load failed (%s): %s", path, exc)
76
+ return None, None
77
+
78
+
79
+ def _env_context_window_tokens() -> int:
80
+ cap_raw = os.environ.get("MODEL_CONTEXT_WINDOW", "200000").strip()
81
+ try:
82
+ return max(1, int(cap_raw))
83
+ except ValueError:
84
+ return 200_000
85
+
86
+
87
+ async def create_harness_deps(
88
+ db_url: str,
89
+ memory_storage_uri: str | None = None,
90
+ *,
91
+ mcp_config_path: Path | None = None,
92
+ provider: str | None = None,
93
+ model: str | None = None,
94
+ open_mcp: bool = True,
95
+ provider_override: Provider | None = None,
96
+ _provider_override: Provider | None = None,
97
+ ) -> HarnessDeps:
98
+ """Open storage, optional memory, MCP, and resolve the LLM provider.
99
+
100
+ Args:
101
+ db_url: Passed to :func:`~monkeybot.core.persistence.backends.create_storage_backend`.
102
+ memory_storage_uri: When set, passed to :func:`~monkeybot.core.workspace.create_workspace_storage`
103
+ and wrapped in :class:`~monkeybot.core.memory.subsystem.MemorySubsystem`.
104
+ mcp_config_path: When ``open_mcp`` is True and this path is a file,
105
+ :meth:`~monkeybot.core.mcp.mcp_client.MCPClient.load_from_config` is called.
106
+ provider: ``MODEL_PROVIDER`` override for :func:`~monkeybot.core.config.settings.get_provider_config`.
107
+ model: Model id override for :func:`~monkeybot.core.config.settings.get_provider_config`.
108
+ open_mcp: When False, MCP is not loaded from disk (typical for Lambda).
109
+ provider_override: Inject a custom :class:`~monkeybot.core.llm.provider.Provider` (skips config lookup).
110
+ _provider_override: Deprecated alias for ``provider_override`` (tests).
111
+ """
112
+ override = provider_override if provider_override is not None else _provider_override
113
+ backend = create_storage_backend(db_url)
114
+ await backend.open(run_schema=auto_schema_enabled_from_config())
115
+ try:
116
+ if override is not None:
117
+ prov = override
118
+ model_str = str(model or "test-model")
119
+ else:
120
+ pc = get_provider_config(provider=provider, model_name=model)
121
+ prov = pc.provider
122
+ model_str = pc.model
123
+
124
+ memory: MemorySubsystem | None = None
125
+ uri = (memory_storage_uri or "").strip()
126
+ if uri:
127
+ ws = create_workspace_storage(uri)
128
+ memory = MemorySubsystem(
129
+ storage=ws,
130
+ provider=prov,
131
+ model=model_str,
132
+ memory_uri=uri,
133
+ )
134
+
135
+ mcp = MCPClient()
136
+ if open_mcp and mcp_config_path is not None:
137
+ strict = os.environ.get("MCP_STRICT_LOAD", "").strip().lower() in ("1", "true", "yes")
138
+ await mcp.load_from_config(mcp_config_path, raise_on_error=strict)
139
+
140
+ return HarnessDeps(
141
+ storage=backend,
142
+ memory=memory,
143
+ mcp=mcp,
144
+ provider=prov,
145
+ model=model_str,
146
+ )
147
+ except BaseException:
148
+ await backend.close()
149
+ raise
150
+
151
+
152
+ async def run_pattern_bc_turn(
153
+ deps: HarnessDeps,
154
+ message: str,
155
+ *,
156
+ session_id: str,
157
+ request_id: str,
158
+ agent_md_path: Path,
159
+ skills_path: Path,
160
+ workspace_root: Path,
161
+ hook_manager: HookManager | None = None,
162
+ ) -> str:
163
+ """Run one user message through :func:`~monkeybot.core.context.build_context` + :func:`~monkeybot.core.runtime.loop.run`.
164
+
165
+ When ``hook_manager`` is ``None`` (typical for Lambda / short-lived handlers), the loop
166
+ does not fire :class:`~monkeybot.core.hooks.HookManager` hooks, so :class:`~monkeybot.core.memory.hook.MemoryHook`
167
+ callbacks (automatic ``POST_TOOL`` / ``POST_TURN`` capture, organizer scheduling) never run.
168
+ ``deps.memory`` still powers the memory index, ``search_memory``, etc. To enable hook-driven
169
+ memory in FaaS, construct a ``HookManager``, ``memory.register_hooks(mgr)``, pass ``mgr``,
170
+ and use positive hook timeouts where needed so work finishes before the handler returns.
171
+
172
+ Returns:
173
+ Concatenated assistant text from :class:`~monkeybot.core.runtime.events.AssistantDelta` events.
174
+
175
+ Raises:
176
+ PatternBcTurnError: When the loop emits an :class:`~monkeybot.core.runtime.events.Error` event.
177
+ """
178
+ ctx = await build_context(
179
+ session_id,
180
+ request_id,
181
+ agent_md_path=agent_md_path,
182
+ memory=deps.memory,
183
+ skills_path=skills_path,
184
+ mcp_client=deps.mcp,
185
+ model=deps.model,
186
+ include_task_tool=False,
187
+ sse_bus=None,
188
+ workspace_root=workspace_root,
189
+ context_window_tokens=_env_context_window_tokens(),
190
+ )
191
+
192
+ run_cmds, run_paths = _resolve_run_command_allowlists()
193
+ executor = CoreToolExecutor(
194
+ workspace_root=workspace_root,
195
+ memory=deps.memory,
196
+ skills_path=skills_path,
197
+ mcp=deps.mcp,
198
+ run_command_allowed_commands=run_cmds,
199
+ run_command_allowed_path_prefixes=run_paths,
200
+ )
201
+
202
+ parts: list[str] = []
203
+ try:
204
+ async for evt in run_loop(
205
+ message,
206
+ ctx,
207
+ provider=deps.provider,
208
+ history=deps.storage.history(),
209
+ inspectors=[],
210
+ tool_executor=executor,
211
+ hook_manager=hook_manager,
212
+ ):
213
+ if isinstance(evt, Error):
214
+ logger.error(
215
+ "run_pattern_bc_turn failed request_id=%s: %s",
216
+ evt.request_id or request_id,
217
+ evt.error,
218
+ )
219
+ raise PatternBcTurnError(evt.error, request_id=evt.request_id or request_id)
220
+ if isinstance(evt, AssistantDelta) and evt.delta:
221
+ parts.append(evt.delta)
222
+ if isinstance(evt, TurnComplete):
223
+ await deps.storage.usage().record(
224
+ session_id,
225
+ deps.model,
226
+ usage_from_totals(evt.usage),
227
+ run_id=request_id,
228
+ )
229
+ finally:
230
+ await executor.aclose()
231
+ if deps.memory is not None:
232
+ await deps.memory.flush()
233
+
234
+ return "".join(parts)
235
+
236
+
237
+ __all__ = [
238
+ "HarnessDeps",
239
+ "PatternBcTurnError",
240
+ "create_harness_deps",
241
+ "run_pattern_bc_turn",
242
+ ]
@@ -0,0 +1,71 @@
1
+ """Configuration utilities for the monkeybot harness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from monkeybot.core.config.realtime_config import (
6
+ RealtimeAudioConfig,
7
+ RealtimeConfig,
8
+ RealtimeMetricsConfig,
9
+ RealtimeModelConfig,
10
+ RealtimeSessionConfig,
11
+ RealtimeWebSocketConfig,
12
+ get_realtime_config,
13
+ )
14
+ from monkeybot.core.config.runtime_env import (
15
+ ENV_MAP,
16
+ apply_monkeybot_runtime_env,
17
+ reset_runtime_env_state_for_tests,
18
+ )
19
+ from monkeybot.core.config.settings import (
20
+ ConfigError,
21
+ CustomMemoryFolder,
22
+ ProviderConfig,
23
+ SubagentConfig,
24
+ auto_schema_enabled_from_config,
25
+ get_provider_config,
26
+ get_subagent_configs,
27
+ get_subagent_registry,
28
+ normalize_model_provider,
29
+ subagent_vertex_google_search_from_config,
30
+ vertex_google_search_enabled_from_config,
31
+ )
32
+ from monkeybot.core.config.validation import (
33
+ SUPPORTED_MODEL_PROVIDERS,
34
+ SUPPORTED_YAML_MODEL_PROVIDERS,
35
+ validate_monkeybot_yaml_doc,
36
+ validate_provider_env,
37
+ )
38
+ from monkeybot.core.config.yaml_loader import (
39
+ load_monkeybot_yaml_dict,
40
+ resolve_monkeybot_config_path,
41
+ )
42
+
43
+ __all__ = [
44
+ "ENV_MAP",
45
+ "ConfigError",
46
+ "CustomMemoryFolder",
47
+ "ProviderConfig",
48
+ "SubagentConfig",
49
+ "SUPPORTED_MODEL_PROVIDERS",
50
+ "SUPPORTED_YAML_MODEL_PROVIDERS",
51
+ "apply_monkeybot_runtime_env",
52
+ "auto_schema_enabled_from_config",
53
+ "get_provider_config",
54
+ "get_subagent_configs",
55
+ "get_subagent_registry",
56
+ "load_monkeybot_yaml_dict",
57
+ "normalize_model_provider",
58
+ "reset_runtime_env_state_for_tests",
59
+ "RealtimeAudioConfig",
60
+ "RealtimeConfig",
61
+ "RealtimeMetricsConfig",
62
+ "RealtimeModelConfig",
63
+ "RealtimeSessionConfig",
64
+ "RealtimeWebSocketConfig",
65
+ "get_realtime_config",
66
+ "resolve_monkeybot_config_path",
67
+ "validate_monkeybot_yaml_doc",
68
+ "validate_provider_env",
69
+ "vertex_google_search_enabled_from_config",
70
+ "subagent_vertex_google_search_from_config",
71
+ ]
@@ -0,0 +1,150 @@
1
+ """Parsed ``realtime`` section of monkeybot.yaml with defaults."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from monkeybot.core.config.yaml_loader import load_monkeybot_yaml_dict
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class RealtimeAudioConfig:
13
+ input_format: str = "pcm_s16le_24khz_mono"
14
+ output_format: str = "pcm_s16le_24khz_mono"
15
+ chunk_ms: int = 200
16
+ max_utterance_sec: int = 60
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class RealtimeSessionConfig:
21
+ max_duration_sec: int = 1800
22
+ idle_timeout_sec: int = 120
23
+ max_response_turn_sec: int = 300
24
+ max_concurrent_sessions: int = 100
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class RealtimeWebSocketConfig:
29
+ enabled: bool = True
30
+ port: int | None = None
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class RealtimeMetricsConfig:
35
+ emit_summary_on_close: bool = True
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class RealtimeModelConfig:
40
+ """Override the model/provider used for the realtime session.
41
+
42
+ Defaults to the main ``model`` section, but some live preview models (e.g.
43
+ gemini-3.1-flash-live-preview) only support the live API and must be kept separate
44
+ from turn-based providers like context curation and memory organizer.
45
+ """
46
+
47
+ name: str | None = None
48
+ provider: str | None = None
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class RealtimeConfig:
53
+ enabled: bool = False
54
+ websocket: RealtimeWebSocketConfig = field(default_factory=RealtimeWebSocketConfig)
55
+ audio: RealtimeAudioConfig = field(default_factory=RealtimeAudioConfig)
56
+ session: RealtimeSessionConfig = field(default_factory=RealtimeSessionConfig)
57
+ metrics: RealtimeMetricsConfig = field(default_factory=RealtimeMetricsConfig)
58
+ model: RealtimeModelConfig = field(default_factory=RealtimeModelConfig)
59
+
60
+
61
+ def _nested_get(mapping: Any, *path: str, default: Any = None) -> Any:
62
+ if not isinstance(mapping, dict):
63
+ return default
64
+ current: Any = mapping
65
+ for part in path:
66
+ if not isinstance(current, dict):
67
+ return default
68
+ current = current.get(part, default)
69
+ if current is None:
70
+ return default
71
+ return current
72
+
73
+
74
+ def _to_bool(value: Any, default: bool) -> bool:
75
+ if isinstance(value, bool):
76
+ return value
77
+ if isinstance(value, str):
78
+ return value.strip().lower() in {"true", "1", "yes", "on"}
79
+ return default
80
+
81
+
82
+ def _to_int(value: Any, default: int) -> int:
83
+ if isinstance(value, int) and not isinstance(value, bool):
84
+ return value
85
+ if isinstance(value, str):
86
+ try:
87
+ return int(value)
88
+ except ValueError:
89
+ return default
90
+ return default
91
+
92
+
93
+ def get_realtime_config(config_path: str | None = None) -> RealtimeConfig:
94
+ """Parse ``realtime.*`` from monkeybot.yaml, applying defaults."""
95
+ _, doc = load_monkeybot_yaml_dict(config_path)
96
+ harness_raw = doc.get("harness")
97
+ harness: dict[str, Any] = harness_raw if isinstance(harness_raw, dict) else {}
98
+ mode = str(harness.get("mode", "turn_based")).strip().lower()
99
+ realtime_raw = doc.get("realtime")
100
+ realtime: dict[str, Any] = realtime_raw if isinstance(realtime_raw, dict) else {}
101
+
102
+ return RealtimeConfig(
103
+ enabled=mode == "realtime",
104
+ websocket=RealtimeWebSocketConfig(
105
+ enabled=_to_bool(_nested_get(realtime, "websocket", "enabled"), default=True),
106
+ port=_to_int(_nested_get(realtime, "websocket", "port"), default=0) or None,
107
+ ),
108
+ audio=RealtimeAudioConfig(
109
+ input_format=str(_nested_get(realtime, "audio", "input_format", default="pcm_s16le_24khz_mono")),
110
+ output_format=str(_nested_get(realtime, "audio", "output_format", default="pcm_s16le_24khz_mono")),
111
+ chunk_ms=_to_int(_nested_get(realtime, "audio", "chunk_ms"), default=200),
112
+ max_utterance_sec=_to_int(
113
+ _nested_get(realtime, "audio", "max_utterance_sec"), default=60
114
+ ),
115
+ ),
116
+ session=RealtimeSessionConfig(
117
+ max_duration_sec=_to_int(
118
+ _nested_get(realtime, "session", "max_duration_sec"), default=1800
119
+ ),
120
+ idle_timeout_sec=_to_int(
121
+ _nested_get(realtime, "session", "idle_timeout_sec"), default=120
122
+ ),
123
+ max_response_turn_sec=_to_int(
124
+ _nested_get(realtime, "session", "max_response_turn_sec"), default=300
125
+ ),
126
+ max_concurrent_sessions=_to_int(
127
+ _nested_get(realtime, "session", "max_concurrent_sessions"), default=100
128
+ ),
129
+ ),
130
+ metrics=RealtimeMetricsConfig(
131
+ emit_summary_on_close=_to_bool(
132
+ _nested_get(realtime, "metrics", "emit_summary_on_close"), default=True
133
+ ),
134
+ ),
135
+ model=RealtimeModelConfig(
136
+ name=_nested_get(realtime, "model", "name"),
137
+ provider=_nested_get(realtime, "model", "provider"),
138
+ ),
139
+ )
140
+
141
+
142
+ __all__ = [
143
+ "RealtimeAudioConfig",
144
+ "RealtimeConfig",
145
+ "RealtimeMetricsConfig",
146
+ "RealtimeModelConfig",
147
+ "RealtimeSessionConfig",
148
+ "RealtimeWebSocketConfig",
149
+ "get_realtime_config",
150
+ ]