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,733 @@
|
|
|
1
|
+
"""Production FastAPI application wiring for the SSE gateway (Story 8).
|
|
2
|
+
|
|
3
|
+
Bootstraps storage backend, MCP, command tiers, and a real :func:`monkeybot.core.runtime.loop.run` driver;
|
|
4
|
+
exposes module-level ``app`` for ``uvicorn monkeybot.gateway.sse.app:app``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import contextlib
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from fastapi import FastAPI
|
|
20
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
21
|
+
|
|
22
|
+
from monkeybot.gateway.bootstrap import ensure_gateway_runtime_env
|
|
23
|
+
|
|
24
|
+
ensure_gateway_runtime_env()
|
|
25
|
+
from monkeybot.core.attachments.config import attachments_enabled_from_env
|
|
26
|
+
from monkeybot.core.attachments.store import AttachmentStore, FilesystemAttachmentStore
|
|
27
|
+
from monkeybot.core.config.settings import (
|
|
28
|
+
ConfigError,
|
|
29
|
+
SubagentConfig,
|
|
30
|
+
auto_schema_enabled_from_config,
|
|
31
|
+
get_provider_config,
|
|
32
|
+
get_subagent_registry,
|
|
33
|
+
normalize_model_provider,
|
|
34
|
+
vertex_google_search_enabled_from_config,
|
|
35
|
+
)
|
|
36
|
+
from monkeybot.core.context import build_context
|
|
37
|
+
from monkeybot.core.hooks import HookManager
|
|
38
|
+
from monkeybot.core.llm.provider import (
|
|
39
|
+
Done,
|
|
40
|
+
Provider,
|
|
41
|
+
ProviderEvent,
|
|
42
|
+
TextDelta,
|
|
43
|
+
ToolCall,
|
|
44
|
+
UsageEvent,
|
|
45
|
+
)
|
|
46
|
+
from monkeybot.core.llm.usage import Usage as UsageRecord
|
|
47
|
+
from monkeybot.core.mcp.mcp_client import MCPClient
|
|
48
|
+
from monkeybot.core.memory.subsystem import MemorySubsystem
|
|
49
|
+
from monkeybot.core.persistence.backends import (
|
|
50
|
+
StorageBackend,
|
|
51
|
+
UsageStore,
|
|
52
|
+
create_storage_backend,
|
|
53
|
+
)
|
|
54
|
+
from monkeybot.core.persistence.transcript import TranscriptWriter, transcript_enabled_from_env
|
|
55
|
+
from monkeybot.core.runtime.events import Error as AgentError
|
|
56
|
+
from monkeybot.core.runtime.events import TurnComplete, UsageTotals, event_to_json
|
|
57
|
+
from monkeybot.core.runtime.loop import SUMMARY_TRIGGER_RATIO
|
|
58
|
+
from monkeybot.core.runtime.loop import run as run_loop
|
|
59
|
+
from monkeybot.core.testing.mocks_provider import ScriptedFakeProvider
|
|
60
|
+
from monkeybot.core.tools.core_tool_executor import CoreToolExecutor
|
|
61
|
+
from monkeybot.core.tools.inspector import CommandTierInspector, RulesInspector, ToolInspector
|
|
62
|
+
from monkeybot.core.tools.permission import try_load_permission_inspector
|
|
63
|
+
from monkeybot.core.tools.loop_inspector import LoopStartInspector
|
|
64
|
+
from monkeybot.core.types.content_blocks import ContentBlock, Text
|
|
65
|
+
from monkeybot.core.workspace import create_workspace_storage
|
|
66
|
+
from monkeybot.gateway.sse.loop_port import UsagePort
|
|
67
|
+
from monkeybot.gateway.sse.routes import create_app as build_sse_app
|
|
68
|
+
from monkeybot.gateway.sse.session_bus import SessionBus, SessionRegistry
|
|
69
|
+
from monkeybot.gateway.sse.workspace_layout import resolve_agent_workspace_root
|
|
70
|
+
from monkeybot.providers.gemini import GeminiProvider
|
|
71
|
+
from monkeybot.web_search import WebSearchTool
|
|
72
|
+
from monkeybot.web_search import build_backend as _build_web_search_backend
|
|
73
|
+
|
|
74
|
+
logger = logging.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class _GatewayDeps:
|
|
79
|
+
"""Process-level deps populated on startup (mutable module singleton)."""
|
|
80
|
+
|
|
81
|
+
mcp: MCPClient | None = None
|
|
82
|
+
inspectors: list[ToolInspector] = field(default_factory=list)
|
|
83
|
+
provider: Provider | None = None
|
|
84
|
+
curator_provider: Provider | None = None
|
|
85
|
+
hook_manager: HookManager | None = None
|
|
86
|
+
memory: MemorySubsystem | None = None
|
|
87
|
+
web_search_tool: WebSearchTool | None = None
|
|
88
|
+
run_command_allowed_commands: list[str] | None = None
|
|
89
|
+
run_command_allowed_path_prefixes: list[str] | None = None
|
|
90
|
+
subagent_registry: dict[str, SubagentConfig] = field(default_factory=dict)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_deps = _GatewayDeps()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _memory_enabled() -> bool:
|
|
97
|
+
"""Default on; explicit off via ``MONKEYBOT_MEMORY_HOOK_ENABLED=false``."""
|
|
98
|
+
raw = os.environ.get("MONKEYBOT_MEMORY_HOOK_ENABLED", "true").strip().lower()
|
|
99
|
+
return raw not in {"0", "false", "no", "off"}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _env_context_window_tokens() -> int:
|
|
103
|
+
cap_raw = os.environ.get("MODEL_CONTEXT_WINDOW", "200000").strip()
|
|
104
|
+
try:
|
|
105
|
+
return max(1, int(cap_raw))
|
|
106
|
+
except ValueError:
|
|
107
|
+
return 200_000
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolved_workspace_paths() -> tuple[Path, Path]:
|
|
111
|
+
"""Resolve agent workspace root and skills dir (relative paths use process ``cwd``)."""
|
|
112
|
+
root = resolve_agent_workspace_root()
|
|
113
|
+
cwd = Path.cwd().resolve()
|
|
114
|
+
skills = Path(os.environ.get("SKILLS_PATH", "skills"))
|
|
115
|
+
skills_p = skills.resolve() if skills.is_absolute() else (cwd / skills).resolve()
|
|
116
|
+
return root, skills_p
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _memory_storage_uri() -> str:
|
|
120
|
+
"""Effective memory storage URI (``MEMORY_STORAGE_URI`` or legacy ``MEMORY_PATH``)."""
|
|
121
|
+
uri = os.environ.get("MEMORY_STORAGE_URI", "").strip()
|
|
122
|
+
if uri:
|
|
123
|
+
return uri
|
|
124
|
+
raw_mp = os.environ.get("MEMORY_PATH")
|
|
125
|
+
if raw_mp is not None:
|
|
126
|
+
logger.info(
|
|
127
|
+
"memory: deriving storage URI from MEMORY_PATH; prefer paths.memory_storage_uri in monkeybot.yaml"
|
|
128
|
+
)
|
|
129
|
+
legacy = (raw_mp or "data/memory").strip()
|
|
130
|
+
mem = Path(legacy)
|
|
131
|
+
resolved = mem.resolve() if mem.is_absolute() else (Path.cwd().resolve() / mem).resolve()
|
|
132
|
+
return f"local://{resolved}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _UsageStoreAdapter(UsagePort):
|
|
136
|
+
"""GET /usage backed by the UsageStore returned from the storage backend."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, store: UsageStore) -> None:
|
|
139
|
+
self._store = store
|
|
140
|
+
|
|
141
|
+
async def session_usage(
|
|
142
|
+
self,
|
|
143
|
+
session_id: str,
|
|
144
|
+
*,
|
|
145
|
+
since: str | None,
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
since_ms: int | None = None
|
|
148
|
+
if since is not None and since.isdigit():
|
|
149
|
+
since_ms = int(since)
|
|
150
|
+
s = await self._store.summary(thread_id=session_id, since_ms=since_ms)
|
|
151
|
+
context_window_tokens = _env_context_window_tokens()
|
|
152
|
+
summarization_threshold_tokens = max(1, int(context_window_tokens * SUMMARY_TRIGGER_RATIO))
|
|
153
|
+
return {
|
|
154
|
+
"session_id": session_id,
|
|
155
|
+
"turns": s.turns,
|
|
156
|
+
"input_tokens": s.input_tokens,
|
|
157
|
+
"output_tokens": s.output_tokens,
|
|
158
|
+
"cached_tokens": s.cached_tokens,
|
|
159
|
+
"cache_read_tokens": s.cache_read_tokens,
|
|
160
|
+
"cache_creation_tokens": s.cache_creation_tokens,
|
|
161
|
+
"cost_usd": s.cost_usd,
|
|
162
|
+
"period_start": s.period_start_ms if s.period_start_ms is not None else 0,
|
|
163
|
+
"period_end": s.period_end_ms if s.period_end_ms is not None else 0,
|
|
164
|
+
"last_prompt_tokens": s.last_prompt_tokens,
|
|
165
|
+
"estimated_prompt_tokens": s.last_estimated_prompt_tokens,
|
|
166
|
+
"summarization_threshold_tokens": summarization_threshold_tokens,
|
|
167
|
+
"context_window_tokens": context_window_tokens,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class _StaticUsagePortZeros(UsagePort):
|
|
172
|
+
"""Pre-startup placeholder so route wiring can construct the FastAPI app."""
|
|
173
|
+
|
|
174
|
+
async def session_usage(
|
|
175
|
+
self,
|
|
176
|
+
session_id: str,
|
|
177
|
+
*,
|
|
178
|
+
since: str | None,
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
del since
|
|
181
|
+
cw = _env_context_window_tokens()
|
|
182
|
+
return {
|
|
183
|
+
"session_id": session_id,
|
|
184
|
+
"turns": 0,
|
|
185
|
+
"input_tokens": 0,
|
|
186
|
+
"output_tokens": 0,
|
|
187
|
+
"cached_tokens": 0,
|
|
188
|
+
"cache_read_tokens": 0,
|
|
189
|
+
"cache_creation_tokens": 0,
|
|
190
|
+
"cost_usd": 0.0,
|
|
191
|
+
"period_start": 0,
|
|
192
|
+
"period_end": 0,
|
|
193
|
+
"last_prompt_tokens": 0,
|
|
194
|
+
"estimated_prompt_tokens": 0,
|
|
195
|
+
"summarization_threshold_tokens": max(1, int(cw * SUMMARY_TRIGGER_RATIO)),
|
|
196
|
+
"context_window_tokens": cw,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _content_blocks_to_text(blocks: list[ContentBlock]) -> str:
|
|
201
|
+
"""Transcript-only rendering of the incoming user turn (non-text blocks summarized)."""
|
|
202
|
+
parts: list[str] = []
|
|
203
|
+
for block in blocks:
|
|
204
|
+
if isinstance(block, Text):
|
|
205
|
+
parts.append(block.text)
|
|
206
|
+
else:
|
|
207
|
+
parts.append(f"[{type(block).__name__}]")
|
|
208
|
+
return "\n".join(parts)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _default_agent_path(bus: SessionBus) -> Path:
|
|
212
|
+
if bus.agent_md:
|
|
213
|
+
return Path(bus.agent_md)
|
|
214
|
+
env = os.environ.get("AGENT_MD")
|
|
215
|
+
if not env:
|
|
216
|
+
raise RuntimeError("AGENT_MD must be set when session has no agent_md path")
|
|
217
|
+
return Path(env)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _scripted_fake_provider() -> Provider:
|
|
221
|
+
"""Deterministic provider for ``MODEL_PROVIDER=fake`` (tests and local dev)."""
|
|
222
|
+
raw = os.environ.get("MONKEYBOT_FAKE_PROVIDER_EVENTS", "")
|
|
223
|
+
if not raw:
|
|
224
|
+
return ScriptedFakeProvider(
|
|
225
|
+
[
|
|
226
|
+
TextDelta(text="hello"),
|
|
227
|
+
UsageEvent(input_tokens=1, output_tokens=2, cached_tokens=0),
|
|
228
|
+
Done(),
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
decoded = json.loads(raw)
|
|
233
|
+
turns: list[list[ProviderEvent]] = []
|
|
234
|
+
for turn in decoded:
|
|
235
|
+
events: list[ProviderEvent] = []
|
|
236
|
+
if not isinstance(turn, list):
|
|
237
|
+
continue
|
|
238
|
+
for item in turn:
|
|
239
|
+
if not isinstance(item, dict):
|
|
240
|
+
continue
|
|
241
|
+
k = item.get("kind")
|
|
242
|
+
if k == "text_delta":
|
|
243
|
+
events.append(TextDelta(text=str(item.get("text", ""))))
|
|
244
|
+
elif k == "usage":
|
|
245
|
+
events.append(
|
|
246
|
+
UsageEvent(
|
|
247
|
+
input_tokens=int(item.get("input_tokens", 0)),
|
|
248
|
+
output_tokens=int(item.get("output_tokens", 0)),
|
|
249
|
+
cached_tokens=int(item.get("cached_tokens", 0)),
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
elif k == "tool_call":
|
|
253
|
+
events.append(
|
|
254
|
+
ToolCall(
|
|
255
|
+
call_id=str(item["call_id"]),
|
|
256
|
+
name=str(item["name"]),
|
|
257
|
+
args=dict(item.get("args", {})),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
elif k == "done":
|
|
261
|
+
events.append(Done())
|
|
262
|
+
if events:
|
|
263
|
+
turns.append(events)
|
|
264
|
+
if not turns:
|
|
265
|
+
turns = [[TextDelta(text="hello"), Done()]]
|
|
266
|
+
flat: list[ProviderEvent] = [ev for turn in turns for ev in turn]
|
|
267
|
+
return ScriptedFakeProvider(flat)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _resolve_provider() -> Provider:
|
|
271
|
+
mode = normalize_model_provider(os.environ.get("MODEL_PROVIDER", "google_vertexai"))
|
|
272
|
+
if mode == "fake":
|
|
273
|
+
return _scripted_fake_provider()
|
|
274
|
+
return get_provider_config(provider=mode).provider
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _resolve_curator_provider(main_provider: Provider) -> Provider:
|
|
278
|
+
"""Dedicated provider for context curation with thinking and token cap overrides.
|
|
279
|
+
|
|
280
|
+
Uses a small ``max_tokens`` (the curator only needs ~50 JSON tokens) and
|
|
281
|
+
``thinking_budget=0`` to explicitly disable extended thinking, which can stall
|
|
282
|
+
preview models for 10s+ on a short JSON-only completion.
|
|
283
|
+
|
|
284
|
+
Fake / vertex_anthropic modes reuse ``main_provider`` — curation is no-op in tests
|
|
285
|
+
and vertex-claude has no thinking budget concept.
|
|
286
|
+
"""
|
|
287
|
+
mode = normalize_model_provider(os.environ.get("MODEL_PROVIDER", "google_vertexai"))
|
|
288
|
+
if mode in ("fake", "vertex_anthropic"):
|
|
289
|
+
return main_provider
|
|
290
|
+
return GeminiProvider(thinking_budget=0, max_tokens=1024)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class GatewayLoopPort:
|
|
294
|
+
"""Schedules :func:`~monkeybot.core.runtime.loop.run` and forwards events to the session bus."""
|
|
295
|
+
|
|
296
|
+
def __init__(self, registry: SessionRegistry) -> None:
|
|
297
|
+
self._registry = registry
|
|
298
|
+
# Bound when this port is used by a non-module app (e.g. create_realtime_app).
|
|
299
|
+
self._fastapi_app: FastAPI | None = None
|
|
300
|
+
|
|
301
|
+
def bind_app(self, fastapi_app: FastAPI) -> None:
|
|
302
|
+
"""Point storage/memory lookups at the serving FastAPI app."""
|
|
303
|
+
self._fastapi_app = fastapi_app
|
|
304
|
+
|
|
305
|
+
def _serving_app(self) -> FastAPI:
|
|
306
|
+
return self._fastapi_app if self._fastapi_app is not None else app
|
|
307
|
+
|
|
308
|
+
async def start_turn(
|
|
309
|
+
self,
|
|
310
|
+
session_id: str,
|
|
311
|
+
request_id: str,
|
|
312
|
+
user_content: list[ContentBlock],
|
|
313
|
+
) -> None:
|
|
314
|
+
bus = self._registry.get(session_id)
|
|
315
|
+
if bus is None:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
mcp = _deps.mcp
|
|
319
|
+
inspectors = _deps.inspectors
|
|
320
|
+
provider = bus.provider or _deps.provider
|
|
321
|
+
|
|
322
|
+
if mcp is None or provider is None:
|
|
323
|
+
logger.error("gateway deps not initialized")
|
|
324
|
+
await bus.publish_data(
|
|
325
|
+
event_to_json(AgentError(request_id=request_id, error="gateway_not_ready"))
|
|
326
|
+
)
|
|
327
|
+
await bus.publish_data(
|
|
328
|
+
event_to_json(TurnComplete(request_id=request_id, usage=UsageTotals()))
|
|
329
|
+
)
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
serving = self._serving_app()
|
|
333
|
+
backend: StorageBackend = serving.state.storage
|
|
334
|
+
history = backend.history()
|
|
335
|
+
usage_store = backend.usage()
|
|
336
|
+
|
|
337
|
+
cancel_event = asyncio.Event()
|
|
338
|
+
|
|
339
|
+
async def _watch_cancel() -> None:
|
|
340
|
+
while True:
|
|
341
|
+
if bus.cancel_requested_for == request_id:
|
|
342
|
+
cancel_event.set()
|
|
343
|
+
return
|
|
344
|
+
await asyncio.sleep(0.05)
|
|
345
|
+
|
|
346
|
+
watcher = asyncio.create_task(_watch_cancel())
|
|
347
|
+
|
|
348
|
+
executor: CoreToolExecutor | None = None
|
|
349
|
+
try:
|
|
350
|
+
model_name = bus.model_name or os.environ.get("MODEL_NAME", "gemini-2.5-flash")
|
|
351
|
+
agent_path = _default_agent_path(bus)
|
|
352
|
+
|
|
353
|
+
workspace_root, skills_resolved = _resolved_workspace_paths()
|
|
354
|
+
|
|
355
|
+
transcript_writer: TranscriptWriter | None = None
|
|
356
|
+
if transcript_enabled_from_env():
|
|
357
|
+
if bus.transcript_writer is None:
|
|
358
|
+
bus.transcript_writer = TranscriptWriter(
|
|
359
|
+
session_id, workspace_root=workspace_root
|
|
360
|
+
)
|
|
361
|
+
transcript_writer = bus.transcript_writer
|
|
362
|
+
await transcript_writer.ensure_manifest(
|
|
363
|
+
agent_md=str(agent_path),
|
|
364
|
+
model=model_name,
|
|
365
|
+
provider=provider.name,
|
|
366
|
+
workspace_root=str(workspace_root),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
attachment_store: AttachmentStore | None = getattr(
|
|
370
|
+
serving.state, "attachment_store", None
|
|
371
|
+
)
|
|
372
|
+
if bus.attachment_catalog is not None:
|
|
373
|
+
rows = await history.load(session_id)
|
|
374
|
+
bus.attachment_catalog.rebuild_from_history(rows)
|
|
375
|
+
|
|
376
|
+
extra_tools = [_deps.web_search_tool] if _deps.web_search_tool is not None else []
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
ctx = await build_context(
|
|
380
|
+
session_id,
|
|
381
|
+
request_id,
|
|
382
|
+
agent_md_path=agent_path,
|
|
383
|
+
memory=getattr(serving.state, "memory", None),
|
|
384
|
+
skills_path=skills_resolved,
|
|
385
|
+
mcp_client=mcp,
|
|
386
|
+
model=model_name,
|
|
387
|
+
cancelled=cancel_event,
|
|
388
|
+
context_window_tokens=_env_context_window_tokens(),
|
|
389
|
+
workspace_root=workspace_root,
|
|
390
|
+
sse_bus=bus,
|
|
391
|
+
extra_tools=extra_tools,
|
|
392
|
+
subagent_registry=_deps.subagent_registry,
|
|
393
|
+
)
|
|
394
|
+
except Exception as exc:
|
|
395
|
+
logger.exception("build_context failed")
|
|
396
|
+
await bus.publish_data(
|
|
397
|
+
event_to_json(AgentError(request_id=request_id, error=str(exc)))
|
|
398
|
+
)
|
|
399
|
+
await bus.publish_data(
|
|
400
|
+
event_to_json(TurnComplete(request_id=request_id, usage=UsageTotals()))
|
|
401
|
+
)
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
storage_backend = getattr(serving.state, "storage", None)
|
|
405
|
+
executor = CoreToolExecutor(
|
|
406
|
+
workspace_root=workspace_root,
|
|
407
|
+
memory=getattr(serving.state, "memory", None),
|
|
408
|
+
skills_path=skills_resolved,
|
|
409
|
+
mcp=mcp,
|
|
410
|
+
extra_tools=extra_tools,
|
|
411
|
+
run_command_allowed_commands=_deps.run_command_allowed_commands,
|
|
412
|
+
run_command_allowed_path_prefixes=_deps.run_command_allowed_path_prefixes,
|
|
413
|
+
attachment_store=attachment_store,
|
|
414
|
+
attachment_catalog=bus.attachment_catalog,
|
|
415
|
+
run_store=storage_backend.runs() if storage_backend is not None else None,
|
|
416
|
+
scheduled_loop_store=(
|
|
417
|
+
storage_backend.scheduled_loops() if storage_backend is not None else None
|
|
418
|
+
),
|
|
419
|
+
subagent_registry=_deps.subagent_registry,
|
|
420
|
+
)
|
|
421
|
+
if transcript_writer is not None:
|
|
422
|
+
await transcript_writer.write_user_message(
|
|
423
|
+
request_id=request_id,
|
|
424
|
+
content=_content_blocks_to_text(user_content),
|
|
425
|
+
)
|
|
426
|
+
async for evt in run_loop(
|
|
427
|
+
user_content,
|
|
428
|
+
ctx,
|
|
429
|
+
provider=provider,
|
|
430
|
+
history=history,
|
|
431
|
+
inspectors=inspectors,
|
|
432
|
+
tool_executor=executor,
|
|
433
|
+
cancelled=cancel_event,
|
|
434
|
+
hook_manager=_deps.hook_manager,
|
|
435
|
+
curator_provider=_deps.curator_provider,
|
|
436
|
+
attachment_store=attachment_store,
|
|
437
|
+
attachment_catalog=bus.attachment_catalog,
|
|
438
|
+
transcript_writer=transcript_writer,
|
|
439
|
+
vertex_google_search=vertex_google_search_enabled_from_config(),
|
|
440
|
+
input_admission=bus.admission,
|
|
441
|
+
):
|
|
442
|
+
if isinstance(evt, TurnComplete):
|
|
443
|
+
u = evt.usage
|
|
444
|
+
await usage_store.record(
|
|
445
|
+
session_id,
|
|
446
|
+
model_name,
|
|
447
|
+
UsageRecord(
|
|
448
|
+
input_tokens=u.input_tokens,
|
|
449
|
+
output_tokens=u.output_tokens,
|
|
450
|
+
cached_tokens=u.cached_tokens,
|
|
451
|
+
cache_read_tokens=u.cache_read_tokens,
|
|
452
|
+
cache_creation_tokens=u.cache_creation_tokens,
|
|
453
|
+
cost_usd=u.cost_usd,
|
|
454
|
+
duration_ms=u.duration_ms,
|
|
455
|
+
estimated_prompt_tokens=u.estimated_prompt_tokens,
|
|
456
|
+
),
|
|
457
|
+
run_id=request_id,
|
|
458
|
+
)
|
|
459
|
+
if transcript_writer is not None:
|
|
460
|
+
await transcript_writer.write_event(evt)
|
|
461
|
+
await bus.publish_data(event_to_json(evt))
|
|
462
|
+
finally:
|
|
463
|
+
if executor is not None:
|
|
464
|
+
await executor.aclose()
|
|
465
|
+
watcher.cancel()
|
|
466
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
467
|
+
await watcher
|
|
468
|
+
bus.cancel_requested_for = None
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _cors_allow_origins() -> list[str]:
|
|
472
|
+
"""Browser origins allowed for cross-origin API calls (SSE, JSON).
|
|
473
|
+
|
|
474
|
+
Comma-separated ``MONKEYBOT_CORS_ALLOW_ORIGINS`` overrides the dev default
|
|
475
|
+
(``http://localhost:5173``). Use when the chat UI is hosted on another origin
|
|
476
|
+
and talks to the gateway directly (without a same-origin dev proxy).
|
|
477
|
+
|
|
478
|
+
Set to ``*`` to allow any origin (credentials disabled by Starlette rules for ``*``).
|
|
479
|
+
"""
|
|
480
|
+
raw = os.environ.get("MONKEYBOT_CORS_ALLOW_ORIGINS", "").strip()
|
|
481
|
+
if not raw:
|
|
482
|
+
return ["http://localhost:5173"]
|
|
483
|
+
if raw == "*":
|
|
484
|
+
return ["*"]
|
|
485
|
+
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
|
486
|
+
return parts if parts else ["http://localhost:5173"]
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _tool_denied_patterns() -> list[str]:
|
|
490
|
+
"""Substring deny list for :class:`RulesInspector`. Empty ``MONKEYBOT_TOOL_DENIED_PATTERNS`` disables."""
|
|
491
|
+
raw = os.environ.get("MONKEYBOT_TOOL_DENIED_PATTERNS")
|
|
492
|
+
if raw is None:
|
|
493
|
+
return ["rm -rf", "/etc/passwd", "DROP TABLE"]
|
|
494
|
+
return [p.strip() for p in raw.split(",") if p.strip()]
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
async def _startup(fastapi_app: FastAPI) -> None:
|
|
498
|
+
"""Wire storage backend, MCP, inspectors, and provider."""
|
|
499
|
+
from monkeybot.observability import init_observability
|
|
500
|
+
|
|
501
|
+
otel_enabled = init_observability()
|
|
502
|
+
if otel_enabled:
|
|
503
|
+
logger.info("OpenTelemetry tracing initialized")
|
|
504
|
+
else:
|
|
505
|
+
logger.info("OpenTelemetry tracing not enabled")
|
|
506
|
+
|
|
507
|
+
db_url = os.environ.get("DB_URL", "sqlite:///data/monkeybot.db")
|
|
508
|
+
|
|
509
|
+
backend = create_storage_backend(db_url)
|
|
510
|
+
await backend.open(run_schema=auto_schema_enabled_from_config())
|
|
511
|
+
fastapi_app.state.storage = backend
|
|
512
|
+
fastapi_app.state.usage = _UsageStoreAdapter(backend.usage())
|
|
513
|
+
|
|
514
|
+
mcp = MCPClient()
|
|
515
|
+
_deps.mcp = mcp
|
|
516
|
+
mcp_config = Path(os.environ.get("MCP_CONFIG", "/app/monkeybot_config/mcp.json"))
|
|
517
|
+
strict = os.environ.get("MCP_STRICT_LOAD", "").strip().lower() in ("1", "true", "yes")
|
|
518
|
+
try:
|
|
519
|
+
await mcp.load_from_config(mcp_config, raise_on_error=strict)
|
|
520
|
+
except OSError as exc:
|
|
521
|
+
logger.info("MCP config skipped (%s): %s", mcp_config, exc)
|
|
522
|
+
|
|
523
|
+
tiers_path = Path(
|
|
524
|
+
os.environ.get("COMMAND_ALLOWLIST_CONFIG", "/app/monkeybot_config/command_allowlist.yaml")
|
|
525
|
+
)
|
|
526
|
+
_deps.run_command_allowed_commands = None
|
|
527
|
+
_deps.run_command_allowed_path_prefixes = None
|
|
528
|
+
inspectors: list[ToolInspector] = []
|
|
529
|
+
try:
|
|
530
|
+
tier_insp = CommandTierInspector(tiers_path)
|
|
531
|
+
inspectors.append(tier_insp)
|
|
532
|
+
_deps.run_command_allowed_commands = list(tier_insp.allowed_commands)
|
|
533
|
+
_deps.run_command_allowed_path_prefixes = list(tier_insp.allowed_path_prefixes)
|
|
534
|
+
except FileNotFoundError:
|
|
535
|
+
logger.info("command tiers missing (%s); allowing all tool calls", tiers_path)
|
|
536
|
+
except Exception as exc:
|
|
537
|
+
logger.exception("command tier load failed: %s", exc)
|
|
538
|
+
|
|
539
|
+
denied = _tool_denied_patterns()
|
|
540
|
+
if denied:
|
|
541
|
+
inspectors.append(RulesInspector(denied))
|
|
542
|
+
|
|
543
|
+
perm_path = Path(
|
|
544
|
+
os.environ.get("PERMISSION_CONFIG", "/app/monkeybot_config/permissions.yaml")
|
|
545
|
+
)
|
|
546
|
+
perm_insp = try_load_permission_inspector(perm_path)
|
|
547
|
+
if perm_insp is not None:
|
|
548
|
+
inspectors.append(perm_insp)
|
|
549
|
+
|
|
550
|
+
inspectors.append(LoopStartInspector())
|
|
551
|
+
_deps.inspectors = inspectors
|
|
552
|
+
|
|
553
|
+
_deps.provider = _resolve_provider()
|
|
554
|
+
_deps.curator_provider = _resolve_curator_provider(_deps.provider)
|
|
555
|
+
|
|
556
|
+
vertex_gs = vertex_google_search_enabled_from_config()
|
|
557
|
+
try:
|
|
558
|
+
backend_ws = _build_web_search_backend()
|
|
559
|
+
if backend_ws is not None:
|
|
560
|
+
_deps.web_search_tool = WebSearchTool(backend_ws)
|
|
561
|
+
logger.info("web search enabled: backend=%s", backend_ws.name)
|
|
562
|
+
else:
|
|
563
|
+
_deps.web_search_tool = None
|
|
564
|
+
except Exception as exc:
|
|
565
|
+
logger.warning("web search backend init failed — disabling: %s", exc)
|
|
566
|
+
_deps.web_search_tool = None
|
|
567
|
+
|
|
568
|
+
if vertex_gs:
|
|
569
|
+
logger.info("vertex google_search grounding enabled")
|
|
570
|
+
if _deps.web_search_tool is None and not vertex_gs:
|
|
571
|
+
logger.info("web search disabled (WEB_SEARCH_BACKEND=none)")
|
|
572
|
+
|
|
573
|
+
if _memory_enabled():
|
|
574
|
+
try:
|
|
575
|
+
mem_uri = _memory_storage_uri()
|
|
576
|
+
storage = create_workspace_storage(mem_uri)
|
|
577
|
+
mgr = HookManager()
|
|
578
|
+
model_name = os.environ.get("MODEL_NAME", "gemini-2.5-flash")
|
|
579
|
+
memory = MemorySubsystem(
|
|
580
|
+
storage=storage,
|
|
581
|
+
provider=_deps.provider,
|
|
582
|
+
model=model_name,
|
|
583
|
+
memory_uri=mem_uri,
|
|
584
|
+
)
|
|
585
|
+
memory.register_hooks(mgr)
|
|
586
|
+
_deps.hook_manager = mgr
|
|
587
|
+
_deps.memory = memory
|
|
588
|
+
fastapi_app.state.memory = memory
|
|
589
|
+
logger.info("memory hook enabled (memory_storage_uri=%s)", mem_uri)
|
|
590
|
+
try:
|
|
591
|
+
gc_stats = await memory.gc_processed()
|
|
592
|
+
if gc_stats["deleted"] or gc_stats["errors"]:
|
|
593
|
+
logger.info(
|
|
594
|
+
"memory gc: scanned=%d deleted=%d errors=%d",
|
|
595
|
+
gc_stats["scanned"],
|
|
596
|
+
gc_stats["deleted"],
|
|
597
|
+
gc_stats["errors"],
|
|
598
|
+
)
|
|
599
|
+
except Exception as gc_exc:
|
|
600
|
+
logger.warning("memory gc on startup failed: %r", gc_exc)
|
|
601
|
+
except Exception as exc:
|
|
602
|
+
logger.warning("memory hook setup failed; continuing without: %r", exc)
|
|
603
|
+
_deps.hook_manager = None
|
|
604
|
+
_deps.memory = None
|
|
605
|
+
fastapi_app.state.memory = None
|
|
606
|
+
else:
|
|
607
|
+
logger.info("memory hook disabled via MONKEYBOT_MEMORY_HOOK_ENABLED")
|
|
608
|
+
fastapi_app.state.memory = None
|
|
609
|
+
|
|
610
|
+
if attachments_enabled_from_env():
|
|
611
|
+
try:
|
|
612
|
+
fastapi_app.state.attachment_store = FilesystemAttachmentStore(
|
|
613
|
+
resolve_agent_workspace_root()
|
|
614
|
+
)
|
|
615
|
+
logger.info("attachments enabled")
|
|
616
|
+
except Exception as exc:
|
|
617
|
+
logger.warning("attachment store init failed: %s", exc)
|
|
618
|
+
fastapi_app.state.attachment_store = None
|
|
619
|
+
else:
|
|
620
|
+
fastapi_app.state.attachment_store = None
|
|
621
|
+
logger.info("attachments disabled via ATTACHMENTS_ENABLED")
|
|
622
|
+
|
|
623
|
+
try:
|
|
624
|
+
_deps.subagent_registry = get_subagent_registry()
|
|
625
|
+
if _deps.subagent_registry:
|
|
626
|
+
logger.info(
|
|
627
|
+
"subagent personas loaded: %s",
|
|
628
|
+
", ".join(sorted(_deps.subagent_registry)),
|
|
629
|
+
)
|
|
630
|
+
except ConfigError as exc:
|
|
631
|
+
logger.error("invalid subagents config: %s", exc)
|
|
632
|
+
raise
|
|
633
|
+
|
|
634
|
+
if otel_enabled:
|
|
635
|
+
from monkeybot.observability.instrumentation import instrument_fastapi_app
|
|
636
|
+
|
|
637
|
+
instrument_fastapi_app(fastapi_app)
|
|
638
|
+
|
|
639
|
+
if os.environ.get("MONKEYBOT_WORKER_POOL", "").strip().lower() in ("1", "true", "yes"):
|
|
640
|
+
# Development-only: runs the subagent worker loop on the gateway's own asyncio
|
|
641
|
+
# event loop, so it competes with SSE streaming under load and has no backpressure.
|
|
642
|
+
# For production, run standalone worker processes (`python -m monkeybot.subagents.worker`)
|
|
643
|
+
# that scale independently of the gateway.
|
|
644
|
+
from monkeybot.core.subagents.worker_pool import (
|
|
645
|
+
start_worker_pool_background,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
logger.warning(
|
|
649
|
+
"MONKEYBOT_WORKER_POOL=1: running the subagent worker pool in-process is "
|
|
650
|
+
"development-only — it shares the gateway event loop with SSE streaming. "
|
|
651
|
+
"For production, run standalone workers via `python -m monkeybot.subagents.worker`."
|
|
652
|
+
)
|
|
653
|
+
fastapi_app.state.worker_pool = start_worker_pool_background(backend)
|
|
654
|
+
|
|
655
|
+
from monkeybot.scheduler.engine import scheduler_enabled_from_env, start_scheduler_background
|
|
656
|
+
from monkeybot.gateway.sse.scheduler_wiring import (
|
|
657
|
+
GatewaySessionEnsurer,
|
|
658
|
+
GatewayTickInvoker,
|
|
659
|
+
StorageSessionBusyChecker,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
if scheduler_enabled_from_env():
|
|
663
|
+
loop_port = GatewayLoopPort(_registry)
|
|
664
|
+
turn_locks = backend.session_turns()
|
|
665
|
+
fastapi_app.state.scheduler = start_scheduler_background(
|
|
666
|
+
store=backend.scheduled_loops(),
|
|
667
|
+
invoker=GatewayTickInvoker(loop_port, _registry, turn_locks),
|
|
668
|
+
session_busy=StorageSessionBusyChecker(turn_locks),
|
|
669
|
+
ensure_session=GatewaySessionEnsurer(_registry),
|
|
670
|
+
)
|
|
671
|
+
logger.info("scheduled-loop engine enabled (in-process; development-friendly)")
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
async def _shutdown(fastapi_app: FastAPI) -> None:
|
|
675
|
+
"""Tear down MCP sessions and storage backend."""
|
|
676
|
+
from monkeybot.observability import shutdown_observability
|
|
677
|
+
|
|
678
|
+
try:
|
|
679
|
+
shutdown_observability()
|
|
680
|
+
except Exception as exc:
|
|
681
|
+
logger.warning("observability shutdown failed: %s", exc)
|
|
682
|
+
|
|
683
|
+
mcp = _deps.mcp
|
|
684
|
+
if mcp is not None:
|
|
685
|
+
for name in list(getattr(mcp, "_servers", {}).keys()):
|
|
686
|
+
await mcp.disconnect(name)
|
|
687
|
+
|
|
688
|
+
worker_pool_handle = getattr(fastapi_app.state, "worker_pool", None)
|
|
689
|
+
if worker_pool_handle is not None:
|
|
690
|
+
from monkeybot.core.subagents.worker_pool import shutdown_worker_pool
|
|
691
|
+
|
|
692
|
+
await shutdown_worker_pool(worker_pool_handle)
|
|
693
|
+
fastapi_app.state.worker_pool = None
|
|
694
|
+
|
|
695
|
+
scheduler_handle = getattr(fastapi_app.state, "scheduler", None)
|
|
696
|
+
if scheduler_handle is not None:
|
|
697
|
+
from monkeybot.scheduler.engine import shutdown_scheduler
|
|
698
|
+
|
|
699
|
+
await shutdown_scheduler(scheduler_handle)
|
|
700
|
+
fastapi_app.state.scheduler = None
|
|
701
|
+
|
|
702
|
+
storage: StorageBackend | None = getattr(fastapi_app.state, "storage", None)
|
|
703
|
+
if storage is not None:
|
|
704
|
+
await storage.close()
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@contextlib.asynccontextmanager
|
|
708
|
+
async def _gateway_lifespan(fastapi_app: FastAPI) -> AsyncIterator[None]:
|
|
709
|
+
await _startup(fastapi_app)
|
|
710
|
+
try:
|
|
711
|
+
yield
|
|
712
|
+
finally:
|
|
713
|
+
await _shutdown(fastapi_app)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
_registry = SessionRegistry()
|
|
717
|
+
app = build_sse_app(
|
|
718
|
+
registry=_registry,
|
|
719
|
+
loop_port=GatewayLoopPort(_registry),
|
|
720
|
+
usage_port=_StaticUsagePortZeros(),
|
|
721
|
+
lifespan=_gateway_lifespan,
|
|
722
|
+
)
|
|
723
|
+
app.add_middleware(
|
|
724
|
+
CORSMiddleware,
|
|
725
|
+
allow_origins=_cors_allow_origins(),
|
|
726
|
+
allow_methods=["*"],
|
|
727
|
+
allow_headers=["*"],
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def create_app() -> FastAPI:
|
|
732
|
+
"""Return the module-level ASGI app (for symmetry with docs/tests)."""
|
|
733
|
+
return app
|