monkeybot 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- monkeybot/__init__.py +3 -0
- monkeybot/cli/__init__.py +3 -0
- monkeybot/cli/__main__.py +8 -0
- monkeybot/cli/audio_io.py +8 -0
- monkeybot/cli/gateway_manager.py +17 -0
- monkeybot/cli/main.py +22 -0
- monkeybot/cli/push_to_talk.py +12 -0
- monkeybot/cli/realtime_client.py +13 -0
- monkeybot/core/__init__.py +19 -0
- monkeybot/core/attachments/__init__.py +22 -0
- monkeybot/core/attachments/catalog.py +62 -0
- monkeybot/core/attachments/config.py +52 -0
- monkeybot/core/attachments/freeze.py +158 -0
- monkeybot/core/attachments/resolve.py +70 -0
- monkeybot/core/attachments/store.py +180 -0
- monkeybot/core/attachments/text.py +72 -0
- monkeybot/core/attachments/tools.py +54 -0
- monkeybot/core/bootstrap.py +242 -0
- monkeybot/core/config/__init__.py +71 -0
- monkeybot/core/config/realtime_config.py +150 -0
- monkeybot/core/config/runtime_env.py +262 -0
- monkeybot/core/config/settings.py +341 -0
- monkeybot/core/config/validation.py +249 -0
- monkeybot/core/config/yaml_loader.py +45 -0
- monkeybot/core/context/__init__.py +781 -0
- monkeybot/core/context/campaign_context.py +8 -0
- monkeybot/core/context/common.py +14 -0
- monkeybot/core/context/curator.py +255 -0
- monkeybot/core/context/epoch.py +226 -0
- monkeybot/core/context/memory_prompt.py +222 -0
- monkeybot/core/context/tool_output_policy.py +270 -0
- monkeybot/core/context/tool_result_ingress.py +290 -0
- monkeybot/core/context/tool_shapers.py +361 -0
- monkeybot/core/hooks/__init__.py +261 -0
- monkeybot/core/llm/__init__.py +4 -0
- monkeybot/core/llm/provider.py +296 -0
- monkeybot/core/llm/realtime_provider.py +203 -0
- monkeybot/core/llm/usage.py +57 -0
- monkeybot/core/logging_utils.py +24 -0
- monkeybot/core/mcp/__init__.py +1 -0
- monkeybot/core/mcp/mcp_client.py +1215 -0
- monkeybot/core/mcp/ports_mcp.py +109 -0
- monkeybot/core/memory/__init__.py +24 -0
- monkeybot/core/memory/hook.py +413 -0
- monkeybot/core/memory/index_format.py +104 -0
- monkeybot/core/memory/integrity.py +180 -0
- monkeybot/core/memory/organizer.py +270 -0
- monkeybot/core/memory/storage_ops.py +139 -0
- monkeybot/core/memory/subsystem.py +91 -0
- monkeybot/core/messages/__init__.py +16 -0
- monkeybot/core/messages/convert_provider.py +41 -0
- monkeybot/core/messages/tool_integrity.py +262 -0
- monkeybot/core/messages/transform_context.py +84 -0
- monkeybot/core/path_safety.py +11 -0
- monkeybot/core/persistence/__init__.py +17 -0
- monkeybot/core/persistence/backends.py +236 -0
- monkeybot/core/persistence/db.py +28 -0
- monkeybot/core/persistence/durable_runs.py +286 -0
- monkeybot/core/persistence/firestore.py +658 -0
- monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
- monkeybot/core/persistence/history.py +156 -0
- monkeybot/core/persistence/postgres.py +895 -0
- monkeybot/core/persistence/runs.py +76 -0
- monkeybot/core/persistence/scheduled_loops.py +435 -0
- monkeybot/core/persistence/session_turn_locks.py +94 -0
- monkeybot/core/persistence/sqlite.py +218 -0
- monkeybot/core/persistence/sqlite_backend.py +74 -0
- monkeybot/core/persistence/thread_summary.py +61 -0
- monkeybot/core/persistence/transcript.py +194 -0
- monkeybot/core/persistence/usage.py +149 -0
- monkeybot/core/prompts/__init__.py +1 -0
- monkeybot/core/prompts/harness_prompt.py +197 -0
- monkeybot/core/prompts/prompt.py +215 -0
- monkeybot/core/runtime/__init__.py +1 -0
- monkeybot/core/runtime/context_budget.py +267 -0
- monkeybot/core/runtime/events.py +819 -0
- monkeybot/core/runtime/input_admission.py +154 -0
- monkeybot/core/runtime/loop.py +2374 -0
- monkeybot/core/runtime/provider_stream_mapper.py +159 -0
- monkeybot/core/runtime/realtime_loop.py +654 -0
- monkeybot/core/runtime/utterance_buffer.py +179 -0
- monkeybot/core/subagents/__init__.py +1 -0
- monkeybot/core/subagents/subagent_proto.py +331 -0
- monkeybot/core/subagents/subagent_worker.py +441 -0
- monkeybot/core/subagents/worker_pool.py +403 -0
- monkeybot/core/testing/__init__.py +1 -0
- monkeybot/core/testing/mocks_provider.py +86 -0
- monkeybot/core/testing/mocks_realtime_provider.py +137 -0
- monkeybot/core/tools/__init__.py +1 -0
- monkeybot/core/tools/core_tool_executor.py +1548 -0
- monkeybot/core/tools/inspector.py +226 -0
- monkeybot/core/tools/loop_inspector.py +45 -0
- monkeybot/core/tools/patch.py +480 -0
- monkeybot/core/tools/permission.py +284 -0
- monkeybot/core/tools/sandbox_executor.py +255 -0
- monkeybot/core/tools/spill_inventory.py +35 -0
- monkeybot/core/tools/terminal.py +381 -0
- monkeybot/core/tools/text_normalize.py +25 -0
- monkeybot/core/tools/types.py +33 -0
- monkeybot/core/tools/workspace_service.py +710 -0
- monkeybot/core/tools/workspace_tools.py +116 -0
- monkeybot/core/types/__init__.py +1 -0
- monkeybot/core/types/content_blocks.py +644 -0
- monkeybot/core/types/interfaces.py +156 -0
- monkeybot/core/types/types_tools.py +29 -0
- monkeybot/core/workspace/__init__.py +8 -0
- monkeybot/core/workspace/factory.py +45 -0
- monkeybot/core/workspace/gcs.py +130 -0
- monkeybot/core/workspace/local.py +162 -0
- monkeybot/core/workspace/protocol.py +45 -0
- monkeybot/core/workspace/s3.py +151 -0
- monkeybot/core/workspace_layout.py +27 -0
- monkeybot/gateway/__init__.py +1 -0
- monkeybot/gateway/bootstrap.py +18 -0
- monkeybot/gateway/main.py +47 -0
- monkeybot/gateway/realtime/__init__.py +31 -0
- monkeybot/gateway/realtime/app.py +321 -0
- monkeybot/gateway/realtime/deps.py +52 -0
- monkeybot/gateway/realtime/errors.py +81 -0
- monkeybot/gateway/realtime/guardrails.py +88 -0
- monkeybot/gateway/realtime/manager.py +77 -0
- monkeybot/gateway/realtime/metrics.py +144 -0
- monkeybot/gateway/realtime/routes.py +864 -0
- monkeybot/gateway/realtime/session.py +232 -0
- monkeybot/gateway/realtime/wire.py +412 -0
- monkeybot/gateway/realtime_main.py +49 -0
- monkeybot/gateway/sse/__init__.py +1 -0
- monkeybot/gateway/sse/app.py +733 -0
- monkeybot/gateway/sse/loop_port.py +31 -0
- monkeybot/gateway/sse/models.py +177 -0
- monkeybot/gateway/sse/reply_body.py +91 -0
- monkeybot/gateway/sse/routes.py +1101 -0
- monkeybot/gateway/sse/scheduler_routes.py +200 -0
- monkeybot/gateway/sse/scheduler_wiring.py +96 -0
- monkeybot/gateway/sse/session_bus.py +226 -0
- monkeybot/gateway/sse/sse.py +46 -0
- monkeybot/gateway/sse/workspace_layout.py +7 -0
- monkeybot/observability/__init__.py +220 -0
- monkeybot/observability/_state.py +10 -0
- monkeybot/observability/instrumentation.py +153 -0
- monkeybot/observability/propagation.py +65 -0
- monkeybot/observability/spans.py +455 -0
- monkeybot/providers/__init__.py +19 -0
- monkeybot/providers/_openai_compat.py +450 -0
- monkeybot/providers/_utils.py +473 -0
- monkeybot/providers/bedrock.py +145 -0
- monkeybot/providers/claude.py +125 -0
- monkeybot/providers/gemini.py +677 -0
- monkeybot/providers/gemini_live.py +398 -0
- monkeybot/providers/huggingface.py +129 -0
- monkeybot/providers/nvidia.py +104 -0
- monkeybot/providers/ollama.py +152 -0
- monkeybot/providers/openai.py +127 -0
- monkeybot/providers/pricing.py +60 -0
- monkeybot/providers/sampling.py +44 -0
- monkeybot/providers/vertex_claude.py +148 -0
- monkeybot/scaffold/__init__.py +33 -0
- monkeybot/scheduler/__init__.py +13 -0
- monkeybot/scheduler/__main__.py +4 -0
- monkeybot/scheduler/engine.py +333 -0
- monkeybot/scheduler/http_invoker.py +61 -0
- monkeybot/scheduler/interval.py +77 -0
- monkeybot/scheduler/tick_result.py +34 -0
- monkeybot/scheduler/worker.py +87 -0
- monkeybot/subagents/__init__.py +1 -0
- monkeybot/subagents/worker/__init__.py +1 -0
- monkeybot/subagents/worker/__main__.py +22 -0
- monkeybot/web_search/__init__.py +82 -0
- monkeybot/web_search/backends/__init__.py +5 -0
- monkeybot/web_search/backends/duckduckgo.py +32 -0
- monkeybot/web_search/backends/firecrawl.py +43 -0
- monkeybot/web_search/backends/tavily.py +45 -0
- monkeybot/web_search/protocol.py +25 -0
- monkeybot/web_search/tool.py +56 -0
- monkeybot-2.1.1.dist-info/METADATA +318 -0
- monkeybot-2.1.1.dist-info/RECORD +178 -0
- monkeybot-2.1.1.dist-info/WHEEL +4 -0
- monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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
|
+
]
|