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,819 @@
1
+ """Typed agent events with NDJSON-compatible serialization.
2
+
3
+ Wire format uses discriminator ``type`` per SSE payloads; Python dataclasses use
4
+ field ``kind`` internally. Serialization maps ``kind`` ↔ ``type``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Literal, TypeAlias, cast
12
+
13
+ from monkeybot.core.types.interfaces import MonkeybotError
14
+
15
+
16
+ class EventDecodeError(MonkeybotError):
17
+ """Invalid or unsupported AgentEvent JSON.
18
+
19
+ Incoming payloads may expose the discriminator under ``\"type\"`` (canonical
20
+ SSE field) or ``\"kind\"`` as an alias.
21
+ """
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class UsageTotals:
26
+ """Token and cost rollup attached to TurnComplete."""
27
+
28
+ input_tokens: int = 0
29
+ output_tokens: int = 0
30
+ cached_tokens: int = 0
31
+ cost_usd: float = 0.0
32
+ duration_ms: int = 0
33
+ estimated_prompt_tokens: int = 0
34
+ cache_read_tokens: int = 0
35
+ cache_creation_tokens: int = 0
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class Thinking:
40
+ kind: Literal["Thinking"] = "Thinking"
41
+ request_id: str = ""
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class AssistantDelta:
46
+ kind: Literal["AssistantDelta"] = "AssistantDelta"
47
+ request_id: str = ""
48
+ delta: str = ""
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ToolCallStarted:
53
+ kind: Literal["ToolCallStarted"] = "ToolCallStarted"
54
+ request_id: str = ""
55
+ tool: str = ""
56
+ label: str = ""
57
+ args: dict[str, object] = field(default_factory=dict)
58
+ parse_error: str | None = None
59
+ call_id: str = ""
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ToolCallResult:
64
+ kind: Literal["ToolCallResult"] = "ToolCallResult"
65
+ request_id: str = ""
66
+ tool: str = ""
67
+ result: str = ""
68
+ error: str | None = None
69
+ call_id: str = ""
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class TurnComplete:
74
+ kind: Literal["TurnComplete"] = "TurnComplete"
75
+ request_id: str = ""
76
+ usage: UsageTotals = field(default_factory=UsageTotals)
77
+ trace_id: str | None = None
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class Error:
82
+ kind: Literal["Error"] = "Error"
83
+ request_id: str = ""
84
+ error: str = ""
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class ContextSummarizing:
89
+ kind: Literal["ContextSummarizing"] = "ContextSummarizing"
90
+ request_id: str = ""
91
+ estimated_tokens: int = 0
92
+ """Pre-stream input token count that tripped the summarization threshold (same basis as ``estimated_prompt_tokens`` on usage)."""
93
+ context_window_tokens: int = 0
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class ContextSummarized:
98
+ kind: Literal["ContextSummarized"] = "ContextSummarized"
99
+ request_id: str = ""
100
+ turns_summarized: int = 0
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class SystemPromptSnapshot:
105
+ """Full system message text sent to the provider on this inner-loop iteration (playground / debug)."""
106
+
107
+ kind: Literal["SystemPromptSnapshot"] = "SystemPromptSnapshot"
108
+ request_id: str = ""
109
+ inner_turn: int = 0
110
+ """1-based inner loop index for this user message (increments on tool follow-ups)."""
111
+ text: str = ""
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class ImageBlock:
116
+ kind: Literal["ImageBlock"] = "ImageBlock"
117
+ request_id: str = ""
118
+ image_id: str = ""
119
+ mime_type: str = ""
120
+ data: str = ""
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class ThinkingBlockDelta:
125
+ kind: Literal["ThinkingBlockDelta"] = "ThinkingBlockDelta"
126
+ request_id: str = ""
127
+ text: str = ""
128
+ signature: str | None = None
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class ThinkingBlockComplete:
133
+ kind: Literal["ThinkingBlockComplete"] = "ThinkingBlockComplete"
134
+ request_id: str = ""
135
+ signature: str = ""
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class RedactedThinkingBlock:
140
+ kind: Literal["RedactedThinkingBlock"] = "RedactedThinkingBlock"
141
+ request_id: str = ""
142
+ data: str = ""
143
+
144
+
145
+ @dataclass(frozen=True)
146
+ class ToolConfirmationRequestEvent:
147
+ kind: Literal["ToolConfirmationRequest"] = "ToolConfirmationRequest"
148
+ request_id: str = ""
149
+ tool_call_id: str = ""
150
+ tool_name: str = ""
151
+ arguments: dict[str, object] = field(default_factory=dict)
152
+ prompt: str | None = None
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class ActionRequiredEvent:
157
+ kind: Literal["ActionRequiredEvent"] = "ActionRequiredEvent"
158
+ request_id: str = ""
159
+ action_type: Literal["elicitation", "toolConfirmation", "elicitationResponse"] = "elicitation"
160
+ id: str = ""
161
+ payload: dict[str, object] = field(default_factory=dict)
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class FrontendToolRequestEvent:
166
+ kind: Literal["FrontendToolRequest"] = "FrontendToolRequest"
167
+ request_id: str = ""
168
+ tool_call_id: str = ""
169
+ name: str = ""
170
+ args: dict[str, object] = field(default_factory=dict)
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class SystemNotificationEvent:
175
+ kind: Literal["SystemNotificationEvent"] = "SystemNotificationEvent"
176
+ request_id: str = ""
177
+ notification_type: Literal["thinkingMessage", "inlineMessage", "creditsExhausted"] = "inlineMessage"
178
+ msg: str = ""
179
+ data: dict[str, object] | None = None
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class GroundingEvent:
184
+ """Provider-native web-search grounding metadata (e.g. Gemini ``google_search``).
185
+
186
+ Additive: distinct from the harness's ``web_search`` custom tool (``ToolCallStarted``/
187
+ ``ToolCallResult``); this surfaces citations from a provider-hosted search invoked
188
+ server-side during the model call.
189
+ """
190
+
191
+ kind: Literal["GroundingEvent"] = "GroundingEvent"
192
+ request_id: str = ""
193
+ sources: list[dict[str, str]] = field(default_factory=list)
194
+ search_queries: list[str] = field(default_factory=list)
195
+
196
+
197
+ @dataclass(frozen=True)
198
+ class AttachmentDescriptorEvent:
199
+ """Frozen attachment metadata for playground UI (not persisted as a ContentBlock)."""
200
+
201
+ kind: Literal["AttachmentDescriptor"] = "AttachmentDescriptor"
202
+ request_id: str = ""
203
+ attachment_id: str = ""
204
+ mime_type: str = ""
205
+ filename: str = ""
206
+ description: str = ""
207
+
208
+
209
+ @dataclass(frozen=True)
210
+ class UserSteered:
211
+ """User text injected mid-turn at a safe loop boundary (steer queue)."""
212
+
213
+ kind: Literal["UserSteered"] = "UserSteered"
214
+ request_id: str = ""
215
+ text: str = ""
216
+
217
+
218
+ @dataclass(frozen=True)
219
+ class QueuedInputAccepted:
220
+ """Steer or follow-up prompt accepted into a session admission queue."""
221
+
222
+ kind: Literal["QueuedInputAccepted"] = "QueuedInputAccepted"
223
+ request_id: str = ""
224
+ queue: Literal["steer", "follow_up"] = "follow_up"
225
+ position: int = 0
226
+
227
+
228
+ @dataclass(frozen=True)
229
+ class ContextEpochStarted:
230
+ """New context epoch opened (session start, post-compaction, or stable-source change)."""
231
+
232
+ kind: Literal["ContextEpochStarted"] = "ContextEpochStarted"
233
+ request_id: str = ""
234
+ epoch_id: int = 0
235
+ changed_sources: list[str] = field(default_factory=list)
236
+
237
+
238
+ @dataclass(frozen=True)
239
+ class SystemContextUpdated:
240
+ """Volatile system-context sources changed within the current epoch."""
241
+
242
+ kind: Literal["SystemContextUpdated"] = "SystemContextUpdated"
243
+ request_id: str = ""
244
+ epoch_id: int = 0
245
+ changed_sources: list[str] = field(default_factory=list)
246
+
247
+
248
+ @dataclass(frozen=True)
249
+ class AssistantTextStarted:
250
+ """Start of an assistant text block (additive; ``AssistantDelta`` still streams)."""
251
+
252
+ kind: Literal["AssistantTextStarted"] = "AssistantTextStarted"
253
+ request_id: str = ""
254
+
255
+
256
+ @dataclass(frozen=True)
257
+ class AssistantTextEnded:
258
+ """End of an assistant text block (additive).
259
+
260
+ When ``text`` is set, this is a durable settlement boundary (full block text
261
+ for replay). Streaming clients may still reconstruct from ``AssistantDelta``.
262
+ """
263
+
264
+ kind: Literal["AssistantTextEnded"] = "AssistantTextEnded"
265
+ request_id: str = ""
266
+ text: str = ""
267
+
268
+
269
+ @dataclass(frozen=True)
270
+ class ThinkingBlockStarted:
271
+ """Start of a thinking/reasoning block (additive; deltas still use ThinkingBlockDelta)."""
272
+
273
+ kind: Literal["ThinkingBlockStarted"] = "ThinkingBlockStarted"
274
+ request_id: str = ""
275
+
276
+
277
+ @dataclass(frozen=True)
278
+ class ToolInputDeltaEvent:
279
+ """Incremental tool-argument JSON while the model streams a tool call (additive).
280
+
281
+ ``delta`` is a raw, opaque fragment of one streaming JSON document per
282
+ ``call_id`` — not valid JSON by itself. Clients must buffer fragments by
283
+ ``call_id`` (in arrival order) and only attempt to parse once the tool call
284
+ is finalized (``ToolCallStarted``); never parse an individual ``delta``.
285
+ """
286
+
287
+ kind: Literal["ToolInputDelta"] = "ToolInputDelta"
288
+ request_id: str = ""
289
+ call_id: str = ""
290
+ tool: str = ""
291
+ delta: str = ""
292
+
293
+
294
+ AgentEvent: TypeAlias = (
295
+ Thinking
296
+ | AssistantDelta
297
+ | ToolCallStarted
298
+ | ToolCallResult
299
+ | TurnComplete
300
+ | Error
301
+ | ContextSummarizing
302
+ | ContextSummarized
303
+ | SystemPromptSnapshot
304
+ | ImageBlock
305
+ | ThinkingBlockDelta
306
+ | ThinkingBlockComplete
307
+ | RedactedThinkingBlock
308
+ | ToolConfirmationRequestEvent
309
+ | ActionRequiredEvent
310
+ | FrontendToolRequestEvent
311
+ | SystemNotificationEvent
312
+ | AttachmentDescriptorEvent
313
+ | GroundingEvent
314
+ | UserSteered
315
+ | QueuedInputAccepted
316
+ | ContextEpochStarted
317
+ | SystemContextUpdated
318
+ | AssistantTextStarted
319
+ | AssistantTextEnded
320
+ | ThinkingBlockStarted
321
+ | ToolInputDeltaEvent
322
+ )
323
+
324
+ # Durable vs live-only (OpenCode V2-style). Conversation history persists
325
+ # Message/ContentBlock rows separately; this set classifies AgentEvent kinds for
326
+ # transcript filtering and replay semantics.
327
+ #
328
+ # Durable = settlement / boundary events that should survive for debugging replay.
329
+ # Everything else is live-only (streaming fragments and ephemeral UI progress).
330
+ DURABLE_EVENT_KINDS: frozenset[str] = frozenset(
331
+ {
332
+ "ToolCallStarted", # tool input finalized (args known); history mirror = ToolRequest
333
+ "ToolCallResult", # tool success/failure settlement; history mirror = ToolResponse
334
+ "TurnComplete",
335
+ "Error",
336
+ "ContextSummarized",
337
+ "ThinkingBlockComplete",
338
+ "RedactedThinkingBlock",
339
+ "AssistantTextEnded", # when text is set, full settled prose
340
+ "ToolConfirmationRequest",
341
+ "GroundingEvent",
342
+ "UserSteered",
343
+ "QueuedInputAccepted",
344
+ "ContextEpochStarted",
345
+ "SystemContextUpdated",
346
+ }
347
+ )
348
+
349
+
350
+ def is_durable_event(event: AgentEvent) -> bool:
351
+ """Return True when ``event`` is a durable settlement/boundary event."""
352
+ return event.kind in DURABLE_EVENT_KINDS
353
+
354
+
355
+ def _usage_from_obj(raw: object | None) -> UsageTotals:
356
+ if raw is None:
357
+ return UsageTotals()
358
+ if not isinstance(raw, dict):
359
+ return UsageTotals()
360
+ d = raw
361
+ it = int(d.get("input_tokens", 0))
362
+ ot = int(d.get("output_tokens", 0))
363
+ ct = int(d.get("cached_tokens", 0))
364
+ crt = int(d.get("cache_read_tokens", 0))
365
+ cct = int(d.get("cache_creation_tokens", 0))
366
+ cost_raw = d.get("cost_usd", 0.0)
367
+ cost = float(cost_raw) if isinstance(cost_raw, (int, float)) else 0.0
368
+ dur = int(d.get("duration_ms", 0))
369
+ ept = int(d.get("estimated_prompt_tokens", 0))
370
+ return UsageTotals(
371
+ input_tokens=it,
372
+ output_tokens=ot,
373
+ cached_tokens=ct,
374
+ cache_read_tokens=crt,
375
+ cache_creation_tokens=cct,
376
+ cost_usd=cost,
377
+ duration_ms=dur,
378
+ estimated_prompt_tokens=ept,
379
+ )
380
+
381
+
382
+ def _args_from_obj(raw: object | None) -> dict[str, object]:
383
+ if raw is None:
384
+ return {}
385
+ if not isinstance(raw, dict):
386
+ raise EventDecodeError("ToolCallStarted args must be a JSON object")
387
+ return dict(raw)
388
+
389
+
390
+ def _story5_event_dict(event: AgentEvent) -> dict[str, object]:
391
+ """Build JSON-serializable dict for Story 5 SSE types (1B §4.2; snake_case keys)."""
392
+ base: dict[str, object] = {"type": event.kind, "request_id": event.request_id}
393
+ if isinstance(event, ImageBlock):
394
+ out: dict[str, object] = {**base, "mime_type": event.mime_type, "data": event.data}
395
+ if event.image_id:
396
+ out["image_id"] = event.image_id
397
+ return out
398
+ if isinstance(event, ThinkingBlockDelta):
399
+ return {**base, "text": event.text, "signature": event.signature}
400
+ if isinstance(event, ThinkingBlockComplete):
401
+ return {**base, "signature": event.signature}
402
+ if isinstance(event, RedactedThinkingBlock):
403
+ return {**base, "data": event.data}
404
+ if isinstance(event, ToolConfirmationRequestEvent):
405
+ return {
406
+ **base,
407
+ "tool_call_id": event.tool_call_id,
408
+ "tool_name": event.tool_name,
409
+ "arguments": dict(event.arguments),
410
+ "prompt": event.prompt,
411
+ }
412
+ if isinstance(event, ActionRequiredEvent):
413
+ return {
414
+ **base,
415
+ "action_type": event.action_type,
416
+ "id": event.id,
417
+ "payload": dict(event.payload),
418
+ }
419
+ if isinstance(event, FrontendToolRequestEvent):
420
+ return {
421
+ **base,
422
+ "tool_call_id": event.tool_call_id,
423
+ "name": event.name,
424
+ "args": dict(event.args),
425
+ }
426
+ if isinstance(event, SystemNotificationEvent):
427
+ return {
428
+ **base,
429
+ "notification_type": event.notification_type,
430
+ "msg": event.msg,
431
+ "data": event.data,
432
+ }
433
+ if isinstance(event, AttachmentDescriptorEvent):
434
+ return {
435
+ **base,
436
+ "attachment_id": event.attachment_id,
437
+ "mime_type": event.mime_type,
438
+ "filename": event.filename,
439
+ "description": event.description,
440
+ }
441
+ if isinstance(event, GroundingEvent):
442
+ return {
443
+ **base,
444
+ "sources": [dict(s) for s in event.sources],
445
+ "search_queries": list(event.search_queries),
446
+ }
447
+ if isinstance(event, UserSteered):
448
+ return {**base, "text": event.text}
449
+ if isinstance(event, QueuedInputAccepted):
450
+ return {**base, "queue": event.queue, "position": event.position}
451
+ if isinstance(event, ContextEpochStarted):
452
+ return {
453
+ **base,
454
+ "epoch_id": event.epoch_id,
455
+ "changed_sources": list(event.changed_sources),
456
+ }
457
+ if isinstance(event, SystemContextUpdated):
458
+ return {
459
+ **base,
460
+ "epoch_id": event.epoch_id,
461
+ "changed_sources": list(event.changed_sources),
462
+ }
463
+ if isinstance(event, AssistantTextEnded):
464
+ out = dict(base)
465
+ if event.text:
466
+ out["text"] = event.text
467
+ return out
468
+ if isinstance(event, (AssistantTextStarted, ThinkingBlockStarted)):
469
+ return base
470
+ if isinstance(event, ToolInputDeltaEvent):
471
+ return {
472
+ **base,
473
+ "call_id": event.call_id,
474
+ "tool": event.tool,
475
+ "delta": event.delta,
476
+ }
477
+ raise AssertionError(f"_story5_event_dict: unsupported type {type(event)!r}")
478
+
479
+
480
+ def event_to_json(event: AgentEvent) -> str:
481
+ """Serialize ``event`` to a compact JSON line with ``type`` discriminator."""
482
+ base: dict[str, object] = {"type": event.kind, "request_id": event.request_id}
483
+ if isinstance(event, Thinking):
484
+ payload = base
485
+ elif isinstance(event, AssistantDelta):
486
+ payload = {**base, "delta": event.delta}
487
+ elif isinstance(event, ToolCallStarted):
488
+ payload = {**base, "tool": event.tool, "label": event.label, "args": dict(event.args)}
489
+ if event.parse_error is not None:
490
+ payload["parse_error"] = event.parse_error
491
+ if event.call_id:
492
+ payload["call_id"] = event.call_id
493
+ elif isinstance(event, ToolCallResult):
494
+ payload = {**base, "tool": event.tool, "result": event.result, "error": event.error}
495
+ if event.call_id:
496
+ payload["call_id"] = event.call_id
497
+ elif isinstance(event, TurnComplete):
498
+ u = event.usage
499
+ payload = {
500
+ **base,
501
+ "usage": {
502
+ "input_tokens": u.input_tokens,
503
+ "output_tokens": u.output_tokens,
504
+ "cached_tokens": u.cached_tokens,
505
+ "cache_read_tokens": u.cache_read_tokens,
506
+ "cache_creation_tokens": u.cache_creation_tokens,
507
+ "cost_usd": u.cost_usd,
508
+ "duration_ms": u.duration_ms,
509
+ "estimated_prompt_tokens": u.estimated_prompt_tokens,
510
+ },
511
+ }
512
+ if event.trace_id:
513
+ payload["trace_id"] = event.trace_id
514
+ elif isinstance(event, Error):
515
+ payload = {**base, "error": event.error}
516
+ elif isinstance(event, ContextSummarizing):
517
+ payload = {
518
+ **base,
519
+ "estimated_tokens": event.estimated_tokens,
520
+ "context_window_tokens": event.context_window_tokens,
521
+ }
522
+ elif isinstance(event, ContextSummarized):
523
+ payload = {**base, "turns_summarized": event.turns_summarized}
524
+ elif isinstance(event, SystemPromptSnapshot):
525
+ payload = {**base, "inner_turn": event.inner_turn, "text": event.text}
526
+ elif isinstance(
527
+ event,
528
+ (
529
+ ImageBlock,
530
+ ThinkingBlockDelta,
531
+ ThinkingBlockComplete,
532
+ RedactedThinkingBlock,
533
+ ToolConfirmationRequestEvent,
534
+ ActionRequiredEvent,
535
+ FrontendToolRequestEvent,
536
+ SystemNotificationEvent,
537
+ AttachmentDescriptorEvent,
538
+ GroundingEvent,
539
+ UserSteered,
540
+ QueuedInputAccepted,
541
+ ContextEpochStarted,
542
+ SystemContextUpdated,
543
+ AssistantTextStarted,
544
+ AssistantTextEnded,
545
+ ThinkingBlockStarted,
546
+ ToolInputDeltaEvent,
547
+ ),
548
+ ):
549
+ payload = _story5_event_dict(event)
550
+ else:
551
+ raise EventDecodeError(f"Unsupported AgentEvent variant: {type(event).__name__}")
552
+ try:
553
+ return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
554
+ except (TypeError, ValueError) as exc:
555
+ raise EventDecodeError("Failed to serialize event to JSON") from exc
556
+
557
+
558
+ def event_from_json(raw: str) -> AgentEvent:
559
+ """Parse ``raw`` JSON into an AgentEvent (accepts ``type`` or alias ``kind``)."""
560
+ try:
561
+ decoded: Any = json.loads(raw)
562
+ except json.JSONDecodeError as exc:
563
+ raise EventDecodeError("invalid JSON in AgentEvent payload") from exc
564
+ if not isinstance(decoded, dict):
565
+ raise EventDecodeError("AgentEvent payload must be a JSON object")
566
+ payload = decoded
567
+ t = payload.get("type") or payload.get("kind")
568
+ if t is None:
569
+ raise EventDecodeError("missing type in AgentEvent JSON")
570
+ if not isinstance(t, str):
571
+ raise EventDecodeError("event type discriminator must be a string")
572
+ request_id_raw = payload.get("request_id", "")
573
+ rid = request_id_raw if isinstance(request_id_raw, str) else ""
574
+
575
+ if t == "Thinking":
576
+ return Thinking(request_id=rid)
577
+ if t == "AssistantDelta":
578
+ delta_raw = payload.get("delta", "")
579
+ delta = delta_raw if isinstance(delta_raw, str) else ""
580
+ return AssistantDelta(request_id=rid, delta=delta)
581
+ if t == "ToolCallStarted":
582
+ tool_raw = payload.get("tool", "")
583
+ label_raw = payload.get("label", "")
584
+ tool = tool_raw if isinstance(tool_raw, str) else ""
585
+ label = label_raw if isinstance(label_raw, str) else ""
586
+ args = _args_from_obj(payload.get("args"))
587
+ pe_raw = payload.get("parse_error")
588
+ parse_error: str | None = pe_raw if isinstance(pe_raw, str) else None
589
+ call_id_raw = payload.get("call_id", "")
590
+ call_id = call_id_raw if isinstance(call_id_raw, str) else ""
591
+ return ToolCallStarted(
592
+ request_id=rid,
593
+ tool=tool,
594
+ label=label,
595
+ args=args,
596
+ parse_error=parse_error,
597
+ call_id=call_id,
598
+ )
599
+ if t == "ToolCallResult":
600
+ tool_raw = payload.get("tool", "")
601
+ result_raw = payload.get("result", "")
602
+ tool = tool_raw if isinstance(tool_raw, str) else ""
603
+ result = result_raw if isinstance(result_raw, str) else ""
604
+ err_raw = payload.get("error")
605
+ err: str | None
606
+ if err_raw is None:
607
+ err = None
608
+ elif isinstance(err_raw, str):
609
+ err = err_raw
610
+ else:
611
+ raise EventDecodeError("ToolCallResult error must be a string or null")
612
+ call_id_raw = payload.get("call_id", "")
613
+ call_id = call_id_raw if isinstance(call_id_raw, str) else ""
614
+ return ToolCallResult(
615
+ request_id=rid, tool=tool, result=result, error=err, call_id=call_id
616
+ )
617
+ if t == "TurnComplete":
618
+ usage = _usage_from_obj(payload.get("usage"))
619
+ trace_id_raw = payload.get("trace_id")
620
+ trace_id: str | None
621
+ if trace_id_raw is None:
622
+ trace_id = None
623
+ elif isinstance(trace_id_raw, str) and trace_id_raw:
624
+ trace_id = trace_id_raw
625
+ else:
626
+ raise EventDecodeError("TurnComplete trace_id must be a non-empty string")
627
+ return TurnComplete(request_id=rid, usage=usage, trace_id=trace_id)
628
+ if t == "Error":
629
+ err_raw = payload.get("error", "")
630
+ err = err_raw if isinstance(err_raw, str) else ""
631
+ return Error(request_id=rid, error=err)
632
+ if t == "ContextSummarizing":
633
+ et_raw = payload.get("estimated_tokens", 0)
634
+ cwt_raw = payload.get("context_window_tokens", 0)
635
+ et = int(et_raw) if isinstance(et_raw, (int, float)) else 0
636
+ cwt = int(cwt_raw) if isinstance(cwt_raw, (int, float)) else 0
637
+ return ContextSummarizing(
638
+ request_id=rid, estimated_tokens=et, context_window_tokens=cwt
639
+ )
640
+ if t == "ContextSummarized":
641
+ ts_raw = payload.get("turns_summarized", 0)
642
+ ts = int(ts_raw) if isinstance(ts_raw, (int, float)) else 0
643
+ return ContextSummarized(request_id=rid, turns_summarized=ts)
644
+ if t == "SystemPromptSnapshot":
645
+ it_raw = payload.get("inner_turn", 0)
646
+ inner_turn = int(it_raw) if isinstance(it_raw, (int, float)) else 0
647
+ text_raw = payload.get("text", "")
648
+ text = text_raw if isinstance(text_raw, str) else ""
649
+ return SystemPromptSnapshot(request_id=rid, inner_turn=inner_turn, text=text)
650
+ if t == "ImageBlock":
651
+ mt = payload.get("mime_type", "")
652
+ data = payload.get("data", "")
653
+ img_raw = payload.get("image_id", "")
654
+ mime_type = mt if isinstance(mt, str) else ""
655
+ data_s = data if isinstance(data, str) else ""
656
+ image_id = img_raw if isinstance(img_raw, str) else ""
657
+ return ImageBlock(request_id=rid, image_id=image_id, mime_type=mime_type, data=data_s)
658
+ if t == "ThinkingBlockDelta":
659
+ text_raw = payload.get("text", "")
660
+ text = text_raw if isinstance(text_raw, str) else ""
661
+ sig_raw = payload.get("signature")
662
+ signature: str | None
663
+ if sig_raw is None:
664
+ signature = None
665
+ elif isinstance(sig_raw, str):
666
+ signature = sig_raw
667
+ else:
668
+ raise EventDecodeError("ThinkingBlockDelta signature must be a string or null")
669
+ return ThinkingBlockDelta(request_id=rid, text=text, signature=signature)
670
+ if t == "ThinkingBlockComplete":
671
+ sig_raw = payload.get("signature", "")
672
+ sig = sig_raw if isinstance(sig_raw, str) else ""
673
+ return ThinkingBlockComplete(request_id=rid, signature=sig)
674
+ if t == "RedactedThinkingBlock":
675
+ data_raw = payload.get("data", "")
676
+ data_r = data_raw if isinstance(data_raw, str) else ""
677
+ return RedactedThinkingBlock(request_id=rid, data=data_r)
678
+ if t == "ToolConfirmationRequest":
679
+ tc = payload.get("tool_call_id", "")
680
+ tn = payload.get("tool_name", "")
681
+ args_raw = payload.get("arguments")
682
+ if args_raw is None:
683
+ arguments: dict[str, object] = {}
684
+ elif isinstance(args_raw, dict):
685
+ arguments = dict(args_raw)
686
+ else:
687
+ raise EventDecodeError("ToolConfirmationRequest arguments must be an object")
688
+ tool_call_id = tc if isinstance(tc, str) else ""
689
+ tool_name = tn if isinstance(tn, str) else ""
690
+ pr = payload.get("prompt")
691
+ prompt: str | None
692
+ if pr is None:
693
+ prompt = None
694
+ elif isinstance(pr, str):
695
+ prompt = pr
696
+ else:
697
+ raise EventDecodeError("ToolConfirmationRequest prompt must be a string or null")
698
+ return ToolConfirmationRequestEvent(
699
+ request_id=rid,
700
+ tool_call_id=tool_call_id,
701
+ tool_name=tool_name,
702
+ arguments=arguments,
703
+ prompt=prompt,
704
+ )
705
+ if t == "ActionRequiredEvent":
706
+ at_raw = payload.get("action_type", "elicitation")
707
+ if at_raw not in ("elicitation", "toolConfirmation", "elicitationResponse"):
708
+ raise EventDecodeError("ActionRequiredEvent action_type invalid")
709
+ at = cast(Literal["elicitation", "toolConfirmation", "elicitationResponse"], at_raw)
710
+ id_raw = payload.get("id", "")
711
+ el_id = id_raw if isinstance(id_raw, str) else ""
712
+ pl_raw = payload.get("payload")
713
+ if pl_raw is None:
714
+ pl: dict[str, object] = {}
715
+ elif isinstance(pl_raw, dict):
716
+ pl = dict(pl_raw)
717
+ else:
718
+ raise EventDecodeError("ActionRequiredEvent payload must be an object")
719
+ return ActionRequiredEvent(
720
+ request_id=rid, action_type=at, id=el_id, payload=pl
721
+ )
722
+ if t == "FrontendToolRequest":
723
+ tc = payload.get("tool_call_id", "")
724
+ name_raw = payload.get("name", "")
725
+ args_raw = payload.get("args")
726
+ if args_raw is None:
727
+ args_d: dict[str, object] = {}
728
+ elif isinstance(args_raw, dict):
729
+ args_d = dict(args_raw)
730
+ else:
731
+ raise EventDecodeError("FrontendToolRequest args must be an object")
732
+ tool_call_id = tc if isinstance(tc, str) else ""
733
+ name = name_raw if isinstance(name_raw, str) else ""
734
+ return FrontendToolRequestEvent(
735
+ request_id=rid, tool_call_id=tool_call_id, name=name, args=args_d
736
+ )
737
+ if t == "SystemNotificationEvent":
738
+ nt_raw = payload.get("notification_type", "inlineMessage")
739
+ if nt_raw not in ("thinkingMessage", "inlineMessage", "creditsExhausted"):
740
+ raise EventDecodeError("SystemNotificationEvent notification_type invalid")
741
+ nt = cast(Literal["thinkingMessage", "inlineMessage", "creditsExhausted"], nt_raw)
742
+ msg_raw = payload.get("msg", "")
743
+ msg = msg_raw if isinstance(msg_raw, str) else ""
744
+ data_raw = payload.get("data")
745
+ data_obj: dict[str, object] | None
746
+ if data_raw is None:
747
+ data_obj = None
748
+ elif isinstance(data_raw, dict):
749
+ data_obj = dict(data_raw)
750
+ else:
751
+ raise EventDecodeError("SystemNotificationEvent data must be an object or null")
752
+ return SystemNotificationEvent(
753
+ request_id=rid, notification_type=nt, msg=msg, data=data_obj
754
+ )
755
+ if t == "GroundingEvent":
756
+ sources_raw = payload.get("sources")
757
+ sources: list[dict[str, str]] = []
758
+ if isinstance(sources_raw, list):
759
+ for item in sources_raw:
760
+ if isinstance(item, dict):
761
+ sources.append({str(k): str(v) for k, v in item.items()})
762
+ queries_raw = payload.get("search_queries")
763
+ queries = [str(q) for q in queries_raw] if isinstance(queries_raw, list) else []
764
+ return GroundingEvent(request_id=rid, sources=sources, search_queries=queries)
765
+ if t == "AttachmentDescriptor":
766
+ aid = payload.get("attachment_id", "")
767
+ mime = payload.get("mime_type", "")
768
+ fname = payload.get("filename", "")
769
+ desc = payload.get("description", "")
770
+ return AttachmentDescriptorEvent(
771
+ request_id=rid,
772
+ attachment_id=aid if isinstance(aid, str) else "",
773
+ mime_type=mime if isinstance(mime, str) else "",
774
+ filename=fname if isinstance(fname, str) else "",
775
+ description=desc if isinstance(desc, str) else "",
776
+ )
777
+ if t == "UserSteered":
778
+ text_raw = payload.get("text", "")
779
+ text = text_raw if isinstance(text_raw, str) else ""
780
+ return UserSteered(request_id=rid, text=text)
781
+ if t == "QueuedInputAccepted":
782
+ q_raw = payload.get("queue", "follow_up")
783
+ if q_raw not in ("steer", "follow_up"):
784
+ raise EventDecodeError("QueuedInputAccepted queue must be steer or follow_up")
785
+ q = cast(Literal["steer", "follow_up"], q_raw)
786
+ pos_raw = payload.get("position", 0)
787
+ pos = int(pos_raw) if isinstance(pos_raw, (int, float)) else 0
788
+ return QueuedInputAccepted(request_id=rid, queue=q, position=pos)
789
+ if t == "ContextEpochStarted":
790
+ eid_raw = payload.get("epoch_id", 0)
791
+ eid = int(eid_raw) if isinstance(eid_raw, (int, float)) else 0
792
+ src_raw = payload.get("changed_sources")
793
+ changed: list[str] = [str(s) for s in src_raw] if isinstance(src_raw, list) else []
794
+ return ContextEpochStarted(request_id=rid, epoch_id=eid, changed_sources=changed)
795
+ if t == "SystemContextUpdated":
796
+ eid_raw = payload.get("epoch_id", 0)
797
+ eid = int(eid_raw) if isinstance(eid_raw, (int, float)) else 0
798
+ src_raw = payload.get("changed_sources")
799
+ changed = [str(s) for s in src_raw] if isinstance(src_raw, list) else []
800
+ return SystemContextUpdated(request_id=rid, epoch_id=eid, changed_sources=changed)
801
+ if t == "AssistantTextStarted":
802
+ return AssistantTextStarted(request_id=rid)
803
+ if t == "AssistantTextEnded":
804
+ text_raw = payload.get("text", "")
805
+ text = text_raw if isinstance(text_raw, str) else ""
806
+ return AssistantTextEnded(request_id=rid, text=text)
807
+ if t == "ThinkingBlockStarted":
808
+ return ThinkingBlockStarted(request_id=rid)
809
+ if t == "ToolInputDelta":
810
+ cid_raw = payload.get("call_id", "")
811
+ tool_raw = payload.get("tool", "")
812
+ delta_raw = payload.get("delta", "")
813
+ return ToolInputDeltaEvent(
814
+ request_id=rid,
815
+ call_id=cid_raw if isinstance(cid_raw, str) else "",
816
+ tool=tool_raw if isinstance(tool_raw, str) else "",
817
+ delta=delta_raw if isinstance(delta_raw, str) else "",
818
+ )
819
+ raise EventDecodeError(f"unknown AgentEvent type: {t!r}")