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,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}")
|