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 @@
1
+ """System prompt composition and harness text."""
@@ -0,0 +1,197 @@
1
+ """Runtime-owned harness prompt fragment.
2
+
3
+ The agent loop appends :func:`harness_fixed_context` after the operator-authored
4
+ base prompt (AGENT.md) so tool/MCP protocol text lives in code, not in the bot file.
5
+ """
6
+
7
+ import os
8
+ from collections.abc import Sequence
9
+
10
+ HARNESS_TOOL_CALL_PROTOCOL = """
11
+ ### Tool-call protocol (strict)
12
+ - Invoke tools only through the provider's native function-call channel. Never emit tool invocations as JSON or pseudo-XML inside your assistant text; any such text is treated as a normal message and no tool will run.
13
+ - After tool results are returned to you, your next response MUST be natural-language text that addresses the user's request using those results. Do not return another empty turn.
14
+ - If you have nothing more to do, give a short final answer; do not stay silent.
15
+ - **Evidence rule:** Never produce a substantive answer about content you were supposed to fetch but could not. If every tool path to that content failed or errored, tell the user what blocked you and what they need to supply — do not synthesize, guess, or hallucinate the missing content.
16
+ - **Fulfillment rule:** When the user asks for a file or code change and the relevant tools are available, use them. Do not answer with only pasted code and manual save instructions."""
17
+
18
+
19
+ _RUN_COMMAND_EXEC_NOTE_HOST = (
20
+ "**Execution:** on the **gateway host** under the binary/path allowlist only "
21
+ "(no OpenSandbox / no extra VM)."
22
+ )
23
+ _RUN_COMMAND_EXEC_NOTE_SANDBOX = (
24
+ "**Execution:** via **OpenSandbox** (Docker-backed session container). "
25
+ "The workspace root is bind-mounted at the **same absolute path**; still subject to the allowlist."
26
+ )
27
+
28
+ _HARNESS_BODY = """## monkeybot harness (fixed)
29
+
30
+ This block is injected by the host every turn. Prefer the **active tool list** the model receives over any stale summary here.
31
+
32
+ When workspace tools (`read_file`, `write_file`, `run_command`, …) appear in that list, you **have a writable workspace** — not a read-only chat window. Fulfill file and code requests with tools; do not paste full artifacts and instruct the user to save manually unless they explicitly asked to see code in chat.
33
+
34
+ ### Core built-in tools (when present in the active tool list)
35
+ - `read_file` / `write_file` / `replace_in_file` / `glob` / `grep` / `apply_patch` — paths are **workspace-relative** under the workspace root below. **`glob`** lists matching files (prefer over `run_command` + `ls`). **`grep`** searches file contents with a regex (prefer over `run_command` + `grep`). **`apply_patch`** applies a multi-file Codex-style patch (Add / Update / Delete / Move) fail-closed. Do not substitute a code block in chat for a file deliverable.
36
+ - `search_memory` — keyword search under the configured memory directory; prefer this over shell commands for any memory lookup.
37
+ - `list_skills` — resolves the skills root path for installed skills listed under `## Skills` below; read each skill's `SKILL.md` under that root for procedure.
38
+ - `run_command` — allowlisted shell with optional `timeout` (seconds). {run_command_exec_note} Shell starts in **workspace root**; use the paths listed under Runtime paths below — do NOT guess directory names. `cd` is a shell builtin and cannot be used as a bare command; use `bash -c "cd <dir> && <cmd>"` instead. Pass **`argv` as a list** with the binary first (e.g. `{{"argv": ["ls", "."]}}`); do not pass `{{"command": "ls -R", "args": []}}` — that treats `ls -R` as the binary name.
39
+ - `enable_mcp` / `disable_mcp` — connect or drop a server declared in mcp.json by name (e.g. `browser`). Prefer `enable_mcp` over inventing `add_mcp_server` command/args for known servers. New tools appear on the **next model step this turn**.
40
+ - `add_mcp_server` / `remove_mcp_server` — ad-hoc stdio MCP connect/disconnect when the server is not in mcp.json; same next-step refresh.
41
+ - `mcp_status` — lifecycle status for catalogued/connected/failed/needs_auth/disabled servers.
42
+ - `list_mcp_resources` / `list_mcp_resource_templates` / `read_mcp_resource` — browse and read MCP resources (server must be connected; use `enable_mcp` first when catalogued).
43
+ - `list_mcp_prompts` / `get_mcp_prompt` — list and fetch MCP prompt templates from connected servers.
44
+ {catalog_mcp_line}{web_search_line}{task_line}
45
+ ### Workspace deliverables
46
+ - **New file or full rewrite** → `write_file`.
47
+ - **Targeted change to an existing file** → `read_file` then `replace_in_file` (unique match; light fuzzy fallbacks; optional `replace_all`).
48
+ - **Multi-file or multi-hunk edit** → `apply_patch` with a Codex-style `*** Begin Patch` … `*** End Patch` envelope (Add / Update / Delete / Move); fail-closed.
49
+ - Tell the user the workspace-relative path when done.
50
+ - **Do not claim** you lack filesystem access, cannot touch the user's machine, or are limited to "chat-only" output when workspace file tools are in the active tool list.
51
+ - Chat text is for answers and brief excerpts — not a stand-in for a file the user asked you to produce.
52
+
53
+ ### Built-in tool errors (recovery)
54
+ - A tool **failed** whenever its response contains `ok: false` (or `is_error`), even if the call itself "succeeded" (e.g. `run_command` returns `exit_code != 0` with an `ok:false` JSON body in `stdout`). Read the result; do not treat a non-empty response as success.
55
+ - Failed built-in tools often return **JSON** with `ok: false`, `error_kind` (`policy` | `validation` | `runtime`), `message`, and `hint`.
56
+ - If `error_kind` is **policy** (e.g. `run_command` blocked), **do not** retry the identical call; change the command or path per `hint` and the lists in `details`, then retry **once** after a single fix.
57
+ - If `error_kind` is **validation**, fix the argument shape (see `details.example`), then retry once.
58
+ - If `error_kind` is **runtime** (the command/tool ran but failed — e.g. missing config, missing env var, bad exit code, script error), **do not** re-run the identical call. The same inputs will produce the same failure. Either fix the underlying cause if you can act on it, or stop and tell the user exactly what is missing (e.g. an env var, project id, or credential) and what they must set.
59
+ - **No-repeat rule (applies to every tool):** never issue a tool call with the same name and same arguments that already failed this turn. A retry is only allowed after you have changed the command, arguments, or path in response to the error. If you cannot change anything, stop retrying and report the blocker in plain text.
60
+
61
+ ### Runtime paths
62
+ - workspace root: `{workspace_root}`
63
+ - memory storage: `{memory_storage_uri}` — always use `search_memory` to query; only use this URI/path directly in `run_command` for low-level inspection.
64
+
65
+ ### MCP tools
66
+ - Names look like `server__tool` (double underscore).
67
+ - Heavy MCP servers are **on-demand**: call `enable_mcp("name")` before using their `server__*` tools when they are not yet in the active tool list.
68
+ - For server-published context (not callable tools), use `list_mcp_resources` / `read_mcp_resource` and `list_mcp_prompts` / `get_mcp_prompt` after the server is connected. Check `mcp_status` when unsure.
69
+ - MCP tool errors are returned as plain error text (not structured JSON). Any response containing an HTTP error code (4xx / 5xx), "not found", "unauthorized", "forbidden", "permission denied", or similar access/availability signals means the tool **did not return usable data**.
70
+ - When an MCP tool fails: state what failed in one sentence, then stop — do **not** fabricate, infer, or summarize content that the tool was supposed to fetch. If a fallback tool is available and meaningfully different, try it once; otherwise tell the user what is needed to proceed (e.g. correct credentials, a public URL, pasting the content directly).
71
+
72
+ ### Skills
73
+ - Installed skill names are listed under `## Skills` in this prompt. When a task matches one, use `list_skills` to get the skills root, then `read_file` on that skill's `SKILL.md` for procedure before running commands or steps it documents."""
74
+
75
+
76
+ _TASK_LINE = (
77
+ "- `task` — subprocess subagent with the same workspace, memory, and MCP configuration; "
78
+ "pass `subagent_type` to select a named persona (see Subagent personas below). "
79
+ "Returns JSON (summary, errors, usage). Nested `task` is disabled inside a subagent.\n"
80
+ )
81
+
82
+ _WEB_SEARCH_LINE = (
83
+ "- `web_search` — search the web for current information; "
84
+ "returns titles, URLs, and text snippets.\n"
85
+ )
86
+
87
+
88
+ # Always-on terse-emission guidance (Levers 1-2 of the honey writing style).
89
+ # Trimmed to rules + safety carve-outs; no examples (volume is cost). Lives in
90
+ # the stable harness prefix so it caches, and never overrides the evidence or
91
+ # no-repeat rules above. Opt-in via ``emission_style`` (env MONKEYBOT_EMISSION_STYLE).
92
+ _EMISSION_STYLE_BLOCK = """
93
+ ### Emission style (terse)
94
+
95
+ Volume is cost. Default to terse; add detail only when brevity would drop correctness.
96
+
97
+ - **Code:** write the minimum that needs to exist. Prefer stdlib and installed deps over new code; edit over add; one line over a block. No speculative params, "might need it later" branches, or single-caller abstractions.
98
+ - **Prose:** answer first. Drop wind-up, hedging, and restating the request. Use fragments/lists over paragraphs when they carry the same info. Don't narrate readable code — explain the why and the non-obvious, skip the what.
99
+ - **Keep exact, never compress:** code blocks verbatim and runnable; identifiers, paths, commands, versions, and error messages quoted exactly.
100
+
101
+ Never cut (brevity must not break correctness):
102
+ - Input validation at trust boundaries, error handling that prevents data loss, and security/auth checks.
103
+ - The blocker report when every tool path failed — state what failed and what's needed; do not synthesize missing content (see the evidence rule below).
104
+ - Anything the user explicitly asked for.
105
+
106
+ When editing code, keep function bodies — signatures alone are not enough to edit correctly.
107
+ """
108
+
109
+ # Lever 3: dense agent-to-agent handoffs. Only meaningful when the `task` tool
110
+ # is active, so the caller gates this on ``include_task_tool``.
111
+ _EMISSION_AGENT_TO_AGENT_BLOCK = """
112
+ ### Subagent handoffs (dense)
113
+
114
+ When the reader is a subagent or orchestrator (`task` results, not user-facing answers), emit the densest format the receiver parses losslessly:
115
+ - Minified JSON, never pretty-printed.
116
+ - Address records by stable key, not position ("the finding with id X", not "the 37th").
117
+ - Aggregate in code; pass counts, not rows for the receiver to count. Number rows only if positional access is unavoidable.
118
+ - For uniform record arrays, prefer columnar: keys once, then value rows.
119
+ """
120
+
121
+
122
+ def emission_style_terse_from_env() -> bool:
123
+ """True when ``MONKEYBOT_EMISSION_STYLE`` opts into the terse emission block."""
124
+ raw = os.environ.get("MONKEYBOT_EMISSION_STYLE", "").strip().lower()
125
+ return raw in {"terse", "true", "1", "on", "yes"}
126
+
127
+
128
+ def _emission_section(*, emission_style: bool, include_task_tool: bool) -> str:
129
+ """Stable-prefix emission guidance; empty unless terse style is opted in."""
130
+ if not emission_style:
131
+ return ""
132
+ section = _EMISSION_STYLE_BLOCK
133
+ if include_task_tool:
134
+ section += _EMISSION_AGENT_TO_AGENT_BLOCK
135
+ return section
136
+
137
+
138
+ def _subagent_personas_block(personas: Sequence[tuple[str, str]]) -> str:
139
+ if not personas:
140
+ return ""
141
+ lines = ["\n### Subagent personas (`task` tool)", ""]
142
+ for name, description in personas:
143
+ lines.append(f"- `{name}` — {description}")
144
+ lines.append(
145
+ "Pass `subagent_type` on `task` to select a persona. Omit for the default subagent AGENT.md."
146
+ )
147
+ return "\n".join(lines) + "\n"
148
+
149
+
150
+ def harness_fixed_context(
151
+ *,
152
+ include_task_tool: bool,
153
+ include_web_search: bool = False,
154
+ workspace_root: str = "(not set)",
155
+ memory_storage_uri: str = "(not set)",
156
+ run_command_opensandbox: bool = False,
157
+ subagent_personas: Sequence[tuple[str, str]] | None = None,
158
+ emission_style: bool = False,
159
+ catalog_mcp_servers: Sequence[str] | None = None,
160
+ ) -> str:
161
+ """Runtime-owned description of core tools, paths, MCP naming, and strict tool-call rules.
162
+
163
+ ``workspace_root`` and ``memory_storage_uri`` are injected once at
164
+ context-build time so the model always uses correct paths in shell commands.
165
+ ``include_web_search`` should be True when a web search backend is active.
166
+ ``run_command_opensandbox`` should match whether ``run_command`` is routed through
167
+ OpenSandbox (same signal as ``SandboxConfig.from_env().enabled``).
168
+ ``subagent_personas`` lists configured named subagent types for the parent orchestrator.
169
+ ``emission_style`` opts in the terse emission-guidance block (env
170
+ ``MONKEYBOT_EMISSION_STYLE``); the dense agent-to-agent sub-block is also gated
171
+ on ``include_task_tool`` so it only appears when the ``task`` tool is active.
172
+ ``catalog_mcp_servers`` lists mcp.json servers available via ``enable_mcp`` but not
173
+ connected until the model activates them.
174
+ """
175
+ exec_note = _RUN_COMMAND_EXEC_NOTE_SANDBOX if run_command_opensandbox else _RUN_COMMAND_EXEC_NOTE_HOST
176
+ catalog = [n.strip() for n in (catalog_mcp_servers or ()) if n and str(n).strip()]
177
+ if catalog:
178
+ names = ", ".join(f"`{n}`" for n in catalog)
179
+ catalog_mcp_line = (
180
+ f"- Configured MCP servers (call `enable_mcp` before use): {names}.\n"
181
+ )
182
+ else:
183
+ catalog_mcp_line = ""
184
+ body = _HARNESS_BODY.format(
185
+ run_command_exec_note=exec_note,
186
+ catalog_mcp_line=catalog_mcp_line,
187
+ web_search_line=_WEB_SEARCH_LINE if include_web_search else "",
188
+ task_line=_TASK_LINE if include_task_tool else "",
189
+ workspace_root=workspace_root,
190
+ memory_storage_uri=memory_storage_uri,
191
+ )
192
+ personas_block = _subagent_personas_block(subagent_personas or ())
193
+ emission_block = _emission_section(
194
+ emission_style=emission_style,
195
+ include_task_tool=include_task_tool,
196
+ )
197
+ return body.rstrip() + personas_block + emission_block + HARNESS_TOOL_CALL_PROTOCOL
@@ -0,0 +1,215 @@
1
+ """Compose the full system prompt: user AGENT.md plus runtime injections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from monkeybot.core.attachments.catalog import AttachmentRecord
8
+ from monkeybot.core.context import TurnContext
9
+ from monkeybot.core.context.memory_prompt import MemoryPromptSelection
10
+ from monkeybot.core.llm.provider import Message
11
+ from monkeybot.core.prompts.harness_prompt import (
12
+ emission_style_terse_from_env,
13
+ harness_fixed_context,
14
+ )
15
+ from monkeybot.core.tools.sandbox_executor import SandboxConfig
16
+ from monkeybot.core.types.content_blocks import Text
17
+
18
+ # Cap injected user text so long pastes do not dominate the context window.
19
+ _MAX_CURRENT_REQUEST_CHARS = 8000
20
+
21
+ # Volatile-tail section headings, defined once here (where the sections are
22
+ # actually composed) so callers that need to locate the stable/volatile
23
+ # boundary in a flattened prompt string (e.g. Anthropic cache-block splitting
24
+ # in ``providers._utils.split_system_prompt_for_cache``) import these instead
25
+ # of re-declaring the literal strings and risking drift.
26
+ MEMORY_INDEX_HEADING = "\n\n## Memory index\n"
27
+ MEMORY_NUDGE_HEADING = "\n\n## Memory\n"
28
+ SKILLS_HEADING = "\n\n## Skills\n"
29
+ CURRENT_REQUEST_HEADING = "\n\n## Current request\n"
30
+ # Not composed here, but part of the same volatile-tail-heading vocabulary —
31
+ # defined alongside the others so ``providers._utils`` has one import for all.
32
+ RUNTIME_NOTES_HEADING = "\n\n## Runtime notes\n"
33
+
34
+
35
+ def _user_text_flat(m: Message) -> str:
36
+ """Concatenate Text blocks from a user turn (tool results use other block types)."""
37
+ return "".join(b.text for b in m.content if isinstance(b, Text))
38
+
39
+
40
+ def _last_user_content(messages: Sequence[Message]) -> tuple[str, int] | None:
41
+ """Return (flattened text, index) of the last user message that carries text.
42
+
43
+ User rows that contain only tool-result blocks (no Text) are skipped so that
44
+ the originating user request — not the synthetic tool-response turn — is
45
+ used as the "Current request" anchor.
46
+ """
47
+ last: tuple[str, int] | None = None
48
+ for i, m in enumerate(messages):
49
+ if m.role == "user":
50
+ text = _user_text_flat(m)
51
+ if text:
52
+ last = (text, i)
53
+ return last
54
+
55
+
56
+ def latest_user_message_text(messages: Sequence[Message] | None) -> str | None:
57
+ """Return stripped text of the last user message, or None."""
58
+ if not messages:
59
+ return None
60
+ last = _last_user_content(messages)
61
+ if last is None:
62
+ return None
63
+ t = last[0].strip()
64
+ return t or None
65
+
66
+
67
+ def _current_request_block(chat_messages: Sequence[Message] | None) -> str:
68
+ """When the transcript continues after the user's last message, restate it in-system.
69
+
70
+ Avoids duplicating the latest user row when it is already the final message in
71
+ ``chat_messages`` (that row is still sent as a normal user message).
72
+ """
73
+ if not chat_messages:
74
+ return ""
75
+ last_user = _last_user_content(chat_messages)
76
+ if last_user is None:
77
+ return ""
78
+ text, _idx = last_user
79
+ if not text.strip():
80
+ return ""
81
+ last = chat_messages[-1]
82
+ if last.role == "user" and _user_text_flat(last).strip():
83
+ return ""
84
+ clipped = text.strip()
85
+ if len(clipped) > _MAX_CURRENT_REQUEST_CHARS:
86
+ clipped = clipped[:_MAX_CURRENT_REQUEST_CHARS].rstrip() + "\n…(truncated)"
87
+ return (
88
+ f"{CURRENT_REQUEST_HEADING}"
89
+ "The conversation has continued with assistant or tool messages since this "
90
+ "user message; treat it as the active task.\n\n"
91
+ f"{clipped}"
92
+ )
93
+
94
+
95
+ def _session_attachments_block(catalog: Sequence[AttachmentRecord] | None) -> str:
96
+ if not catalog:
97
+ return ""
98
+ lines = [
99
+ f"- {r.attachment_id} ({r.filename}, {r.mime_type}): {r.description}"
100
+ for r in catalog
101
+ ]
102
+ return "\n\n## Session attachments\n" + "\n".join(lines)
103
+
104
+
105
+ def _memory_block(
106
+ ctx: TurnContext,
107
+ memory_selection: MemoryPromptSelection | None,
108
+ ) -> str:
109
+ if memory_selection is not None:
110
+ mem_lines = list(memory_selection.lines)
111
+ else:
112
+ mem_lines = list(ctx.memory_index)
113
+
114
+ memory_bullets = "\n".join(f"- {line}" for line in mem_lines) if mem_lines else ""
115
+ mem_block = f"{MEMORY_INDEX_HEADING}{memory_bullets}" if memory_bullets else ""
116
+ if memory_selection is not None and memory_selection.nudge_search:
117
+ shown = len(memory_selection.lines)
118
+ total = memory_selection.total_lines
119
+ mem_block += (
120
+ f"{MEMORY_NUDGE_HEADING}"
121
+ f"Showing {shown} of {total} index entries "
122
+ f"(coverage {memory_selection.coverage:.0%}, confidence {memory_selection.confidence:.0%}). "
123
+ "Use `search_memory` with keywords when the task may depend on older or unstated context."
124
+ )
125
+ return mem_block
126
+
127
+
128
+ def _skills_section(ctx: TurnContext) -> str:
129
+ skill_lines = [f"- {s.name}" for s in ctx.skills]
130
+ skills_block = "\n".join(skill_lines)
131
+ return f"{SKILLS_HEADING}{skills_block}" if skills_block else ""
132
+
133
+
134
+ def _harness_text(ctx: TurnContext) -> str:
135
+ include_task = any(t.name == "task" for t in ctx.tools)
136
+ include_web_search = any(t.name == "web_search" for t in ctx.tools)
137
+ return harness_fixed_context(
138
+ include_task_tool=include_task,
139
+ include_web_search=include_web_search,
140
+ workspace_root=str(ctx.workspace_root) if ctx.workspace_root is not None else "(not set)",
141
+ memory_storage_uri=ctx.memory.uri if ctx.memory is not None else "(not set)",
142
+ run_command_opensandbox=SandboxConfig.from_env().enabled,
143
+ subagent_personas=ctx.subagent_personas,
144
+ emission_style=emission_style_terse_from_env(),
145
+ catalog_mcp_servers=ctx.catalog_mcp_servers,
146
+ )
147
+
148
+
149
+ def compose_stable_baseline(
150
+ ctx: TurnContext,
151
+ *,
152
+ attachment_catalog: Sequence[AttachmentRecord] | None = None,
153
+ ) -> str:
154
+ """Cacheable prefix: AGENT.md + harness + session attachments."""
155
+ harness = _harness_text(ctx)
156
+ attachments = _session_attachments_block(attachment_catalog)
157
+ return f"{ctx.agent_md}\n\n{harness}{attachments}"
158
+
159
+
160
+ def compose_volatile_tail(
161
+ ctx: TurnContext,
162
+ *,
163
+ chat_messages: Sequence[Message] | None = None,
164
+ memory_selection: MemoryPromptSelection | None = None,
165
+ ) -> str:
166
+ """Volatile tail: memory index + skills + current-request anchor."""
167
+ mem_block = _memory_block(ctx, memory_selection)
168
+ skills_section = _skills_section(ctx)
169
+ task = _current_request_block(chat_messages)
170
+ return f"{mem_block}{skills_section}{task}"
171
+
172
+
173
+ def compose_volatile_tail_parts(
174
+ ctx: TurnContext,
175
+ *,
176
+ chat_messages: Sequence[Message] | None = None,
177
+ memory_selection: MemoryPromptSelection | None = None,
178
+ ) -> dict[str, str]:
179
+ """Same sections as :func:`compose_volatile_tail`, individually named.
180
+
181
+ Lets callers (e.g. ``ContextEpochTracker``) attribute a mid-epoch volatile
182
+ change to the specific source that moved — memory, skills, or the
183
+ current-request anchor — instead of a catch-all "volatile" label.
184
+ """
185
+ return {
186
+ "memory": _memory_block(ctx, memory_selection),
187
+ "skills": _skills_section(ctx),
188
+ "current_request": _current_request_block(chat_messages),
189
+ }
190
+
191
+
192
+ def compose_system_prompt(
193
+ ctx: TurnContext,
194
+ *,
195
+ chat_messages: Sequence[Message] | None = None,
196
+ memory_selection: MemoryPromptSelection | None = None,
197
+ attachment_catalog: Sequence[AttachmentRecord] | None = None,
198
+ ) -> str:
199
+ """Build the system string: AGENT.md, harness, attachments, then volatile tail.
200
+
201
+ ``ctx.agent_md`` is the operator-authored base prompt (typically from AGENT.md).
202
+ Stable sections (harness, attachments) precede volatile curation (memory, skills,
203
+ current-request anchor) so implicit and explicit prompt caching can hit a contiguous
204
+ prefix across turns.
205
+
206
+ When ``memory_selection`` is set, its lines (and optional search nudge) are used
207
+ instead of the full ``ctx.memory_index``. Skill names are always taken from
208
+ ``ctx.skills`` (zero-cost discovery); use ``list_skills``/``read_file`` for the
209
+ skills root path and full ``SKILL.md`` procedure.
210
+ """
211
+ stable = compose_stable_baseline(ctx, attachment_catalog=attachment_catalog)
212
+ volatile = compose_volatile_tail(
213
+ ctx, chat_messages=chat_messages, memory_selection=memory_selection
214
+ )
215
+ return f"{stable}{volatile}"
@@ -0,0 +1 @@
1
+ """Agent turn loop and streaming events."""