rlmness-cli 0.2.0__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 (203) hide show
  1. rlmagent/__init__.py +1 -0
  2. rlmagent/__main__.py +8 -0
  3. rlmagent/acp/__init__.py +1 -0
  4. rlmagent/acp/runtime.py +71 -0
  5. rlmagent/acp/server.py +598 -0
  6. rlmagent/acp/updates.py +128 -0
  7. rlmagent/agentnet.py +237 -0
  8. rlmagent/agents/__init__.py +1 -0
  9. rlmagent/agents/approval.py +90 -0
  10. rlmagent/agents/bridge.py +109 -0
  11. rlmagent/agents/budget.py +212 -0
  12. rlmagent/agents/child_handles.py +46 -0
  13. rlmagent/agents/child_mailbox.py +30 -0
  14. rlmagent/agents/kernel_api.py +156 -0
  15. rlmagent/agents/output.py +34 -0
  16. rlmagent/agents/patch.py +107 -0
  17. rlmagent/agents/prompts.py +156 -0
  18. rlmagent/agents/trace.py +290 -0
  19. rlmagent/agents/tree.py +682 -0
  20. rlmagent/agents/wasm_api.py +105 -0
  21. rlmagent/agents/workspace.py +357 -0
  22. rlmagent/autopilot.py +447 -0
  23. rlmagent/backend.py +946 -0
  24. rlmagent/backend_context.py +74 -0
  25. rlmagent/cli/__init__.py +5 -0
  26. rlmagent/cli/config_cmd.py +177 -0
  27. rlmagent/cli/health.py +156 -0
  28. rlmagent/cli/main.py +1194 -0
  29. rlmagent/cli/update.py +188 -0
  30. rlmagent/clipboard.py +63 -0
  31. rlmagent/config/__init__.py +37 -0
  32. rlmagent/config/config.py +297 -0
  33. rlmagent/config/loader.py +119 -0
  34. rlmagent/config/prompts.py +124 -0
  35. rlmagent/config/secure_input.py +213 -0
  36. rlmagent/config/store.py +275 -0
  37. rlmagent/context/__init__.py +47 -0
  38. rlmagent/context/budget.py +557 -0
  39. rlmagent/contract.py +311 -0
  40. rlmagent/contract_run.py +232 -0
  41. rlmagent/conversation.py +2547 -0
  42. rlmagent/cron.py +77 -0
  43. rlmagent/directives.py +893 -0
  44. rlmagent/discovery.py +356 -0
  45. rlmagent/execution_log.py +66 -0
  46. rlmagent/factory.py +189 -0
  47. rlmagent/factory_graph.py +413 -0
  48. rlmagent/harness/__init__.py +63 -0
  49. rlmagent/harness/contracts/__init__.py +1 -0
  50. rlmagent/harness/contracts/stream.py +129 -0
  51. rlmagent/harness/contracts/tooling.py +127 -0
  52. rlmagent/harness/contracts/transcript/__init__.py +68 -0
  53. rlmagent/harness/contracts/transcript/_base.py +29 -0
  54. rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
  55. rlmagent/harness/contracts/transcript/entries.py +180 -0
  56. rlmagent/harness/contracts/transcript/segments.py +64 -0
  57. rlmagent/harness/contracts/values.py +6 -0
  58. rlmagent/harness/driver.py +397 -0
  59. rlmagent/harness/engine.py +489 -0
  60. rlmagent/harness/mending.py +153 -0
  61. rlmagent/harness/provider/__init__.py +1 -0
  62. rlmagent/harness/provider/base.py +27 -0
  63. rlmagent/harness/provider/wire.py +189 -0
  64. rlmagent/harness/py.typed +0 -0
  65. rlmagent/harness/session/__init__.py +5 -0
  66. rlmagent/harness/session/index.py +218 -0
  67. rlmagent/harness/session/lease.py +66 -0
  68. rlmagent/harness/session/records.py +118 -0
  69. rlmagent/harness/session/replay.py +165 -0
  70. rlmagent/harness/session/store.py +155 -0
  71. rlmagent/harness/session/summary.py +46 -0
  72. rlmagent/hints.py +47 -0
  73. rlmagent/history.py +92 -0
  74. rlmagent/hooks/__init__.py +1 -0
  75. rlmagent/hooks/hooks.py +1 -0
  76. rlmagent/instructions.py +157 -0
  77. rlmagent/job_runner.py +117 -0
  78. rlmagent/jobs.py +365 -0
  79. rlmagent/kept_agents.py +397 -0
  80. rlmagent/kernel/__init__.py +5 -0
  81. rlmagent/kernel/session.py +205 -0
  82. rlmagent/kernel/wasm.py +169 -0
  83. rlmagent/logs.py +208 -0
  84. rlmagent/managed_release.py +368 -0
  85. rlmagent/mcp.py +411 -0
  86. rlmagent/mcp_oauth.py +213 -0
  87. rlmagent/model/__init__.py +29 -0
  88. rlmagent/model/_claude/__init__.py +1 -0
  89. rlmagent/model/_claude/courier.py +117 -0
  90. rlmagent/model/_claude/emitter.py +378 -0
  91. rlmagent/model/_oai/__init__.py +1 -0
  92. rlmagent/model/_oai/helpers.py +256 -0
  93. rlmagent/model/_oai/normalize.py +263 -0
  94. rlmagent/model/_oai/parsers.py +317 -0
  95. rlmagent/model/_oai/payloads.py +226 -0
  96. rlmagent/model/_oai/transport.py +127 -0
  97. rlmagent/model/claude.py +552 -0
  98. rlmagent/model/copilot.py +142 -0
  99. rlmagent/model/correlation.py +21 -0
  100. rlmagent/model/deadline.py +19 -0
  101. rlmagent/model/discovery.py +181 -0
  102. rlmagent/model/external/__init__.py +11 -0
  103. rlmagent/model/external/acp.py +202 -0
  104. rlmagent/model/external/acp_install.py +109 -0
  105. rlmagent/model/external/cli.py +248 -0
  106. rlmagent/model/external/textcalls.py +57 -0
  107. rlmagent/model/failover.py +129 -0
  108. rlmagent/model/google.py +199 -0
  109. rlmagent/model/limits.py +46 -0
  110. rlmagent/model/oai_compatible.py +414 -0
  111. rlmagent/model/preferences.py +56 -0
  112. rlmagent/model/py.typed +0 -0
  113. rlmagent/model/registered_oauth.py +195 -0
  114. rlmagent/model/scripted.py +55 -0
  115. rlmagent/model/settings.py +271 -0
  116. rlmagent/model/subscription.py +398 -0
  117. rlmagent/model/token_bounds.py +15 -0
  118. rlmagent/model/transport/__init__.py +1 -0
  119. rlmagent/model/transport/backoff.py +87 -0
  120. rlmagent/model/transport/client.py +87 -0
  121. rlmagent/model/transport/faults.py +65 -0
  122. rlmagent/model/transport/responses_socket.py +223 -0
  123. rlmagent/model/vertex.py +93 -0
  124. rlmagent/network.py +123 -0
  125. rlmagent/packages.py +246 -0
  126. rlmagent/planning.py +84 -0
  127. rlmagent/playbook/__init__.py +1 -0
  128. rlmagent/playbook/apply.py +106 -0
  129. rlmagent/playbook/digest.py +63 -0
  130. rlmagent/playbook/improver.py +272 -0
  131. rlmagent/playbook/plan.py +99 -0
  132. rlmagent/playbook/store.py +67 -0
  133. rlmagent/plugins.py +327 -0
  134. rlmagent/policy.py +105 -0
  135. rlmagent/prompt_refs.py +57 -0
  136. rlmagent/prompts.py +331 -0
  137. rlmagent/prune.py +60 -0
  138. rlmagent/py.typed +0 -0
  139. rlmagent/reasoning.py +135 -0
  140. rlmagent/refinement/__init__.py +1 -0
  141. rlmagent/refinement/controller.py +448 -0
  142. rlmagent/refinement/engine.py +220 -0
  143. rlmagent/refinement/evidence.py +131 -0
  144. rlmagent/refinement/scoped.py +90 -0
  145. rlmagent/refinement/store.py +193 -0
  146. rlmagent/refresh.py +202 -0
  147. rlmagent/routing.py +52 -0
  148. rlmagent/runtime.py +575 -0
  149. rlmagent/safety.py +267 -0
  150. rlmagent/session_html.py +57 -0
  151. rlmagent/session_import.py +52 -0
  152. rlmagent/session_share.py +55 -0
  153. rlmagent/sessions.py +249 -0
  154. rlmagent/shell_sandbox.py +88 -0
  155. rlmagent/skillset.py +320 -0
  156. rlmagent/steering.py +148 -0
  157. rlmagent/stops.py +62 -0
  158. rlmagent/subscriptions.py +219 -0
  159. rlmagent/tasks.py +98 -0
  160. rlmagent/toolhost.py +310 -0
  161. rlmagent/tools/__init__.py +56 -0
  162. rlmagent/tools/_shared.py +210 -0
  163. rlmagent/tools/files.py +462 -0
  164. rlmagent/tools/kernel.py +93 -0
  165. rlmagent/tools/patch_tool.py +68 -0
  166. rlmagent/tools/search.py +188 -0
  167. rlmagent/trust.py +383 -0
  168. rlmagent/tui/__init__.py +1 -0
  169. rlmagent/tui/adapter.py +506 -0
  170. rlmagent/tui/app.py +711 -0
  171. rlmagent/tui/approval_screen.py +57 -0
  172. rlmagent/tui/choice_screen.py +47 -0
  173. rlmagent/tui/completions.py +116 -0
  174. rlmagent/tui/components.py +25 -0
  175. rlmagent/tui/composer.py +40 -0
  176. rlmagent/tui/factory_view.py +86 -0
  177. rlmagent/tui/inspect.py +193 -0
  178. rlmagent/tui/messages.py +215 -0
  179. rlmagent/tui/model_screen.py +307 -0
  180. rlmagent/tui/prompt.py +98 -0
  181. rlmagent/tui/question_screen.py +52 -0
  182. rlmagent/tui/replay.py +141 -0
  183. rlmagent/tui/rlm_agent.tcss +405 -0
  184. rlmagent/tui/search_box.py +91 -0
  185. rlmagent/tui/sidebar.py +182 -0
  186. rlmagent/tui/status.py +49 -0
  187. rlmagent/tui/steps.py +283 -0
  188. rlmagent/tui/suggest.py +778 -0
  189. rlmagent/tui/transcript.py +172 -0
  190. rlmagent/tui/trust_dialog.py +58 -0
  191. rlmagent/tui/worker_attach.py +141 -0
  192. rlmagent/ui/__init__.py +1 -0
  193. rlmagent/ui/render.py +211 -0
  194. rlmagent/undo.py +109 -0
  195. rlmagent/utils/__init__.py +1 -0
  196. rlmagent/websearch.py +75 -0
  197. rlmagent/worker.py +1275 -0
  198. rlmagent/worker_service.py +203 -0
  199. rlmness_cli-0.2.0.dist-info/METADATA +882 -0
  200. rlmness_cli-0.2.0.dist-info/RECORD +203 -0
  201. rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
  202. rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
  203. rlmness_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,117 @@
1
+ """HTTP transport for the Anthropic Messages API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import AsyncIterator
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from rlmagent.harness.contracts.tooling import CancelToken
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Retriable status codes
15
+ # ---------------------------------------------------------------------------
16
+
17
+ _TRANSIENT_CODES = frozenset({408, 409, 425, 429})
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Error type
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ class ApiRejection(Exception):
26
+ """Non-200 HTTP response from the Anthropic Messages API."""
27
+
28
+ def __init__(
29
+ self,
30
+ status: int,
31
+ detail: str,
32
+ *,
33
+ wait_hint: float | None = None,
34
+ ) -> None:
35
+ self.status = status
36
+ self.detail = detail
37
+ self.wait_hint = wait_hint
38
+ super().__init__(f"HTTP {status}")
39
+
40
+ @property
41
+ def retriable(self) -> bool:
42
+ """Return whether this status code warrants an automatic reattempt."""
43
+ return self.status in _TRANSIENT_CODES or self.status >= 500
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Internal helpers
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ def _parse_object(text: str) -> dict[str, Any] | None:
52
+ try:
53
+ obj = json.loads(text)
54
+ return obj if isinstance(obj, dict) else None
55
+ except (json.JSONDecodeError, ValueError):
56
+ return None
57
+
58
+
59
+ def _read_wait_hint(response: httpx.Response) -> float | None:
60
+ raw = response.headers.get("retry-after")
61
+ if raw is None:
62
+ return None
63
+ try:
64
+ return max(0.0, float(raw))
65
+ except (ValueError, TypeError):
66
+ return None
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Streaming transport
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ async def relay_sse(
75
+ client: httpx.AsyncClient,
76
+ url: str,
77
+ body: dict[str, Any],
78
+ headers: dict[str, str],
79
+ *,
80
+ signal: CancelToken | None = None,
81
+ ) -> AsyncIterator[tuple[str, dict[str, Any]]]:
82
+ """POST *body* as streaming JSON and yield ``(event_type, payload)`` pairs."""
83
+ async with client.stream("POST", url, json=body, headers=headers) as response:
84
+ if response.status_code != 200:
85
+ raw = (await response.aread()).decode("utf-8", errors="replace")
86
+ raise ApiRejection(
87
+ response.status_code,
88
+ raw,
89
+ wait_hint=_read_wait_hint(response),
90
+ )
91
+
92
+ event_name = ""
93
+ data_parts: list[str] = []
94
+
95
+ async for raw_line in response.aiter_lines():
96
+ if signal is not None and signal.is_cancelled():
97
+ return
98
+
99
+ line = raw_line.rstrip("\r\n")
100
+
101
+ if not line:
102
+ # Empty line fires the accumulated event
103
+ if data_parts:
104
+ payload = _parse_object("\n".join(data_parts))
105
+ if payload is not None:
106
+ yield (event_name, payload)
107
+ event_name = ""
108
+ data_parts = []
109
+ continue
110
+
111
+ if line.startswith(":"):
112
+ continue # comment / keep-alive
113
+
114
+ if line.startswith("event:"):
115
+ event_name = line[6:].strip()
116
+ elif line.startswith("data:"):
117
+ data_parts.append(line[5:].lstrip())
@@ -0,0 +1,378 @@
1
+ """State-machine parser that converts Anthropic SSE events into rlm-agent wire events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum, auto
8
+ from typing import Any
9
+
10
+ from rlmagent.harness.contracts.transcript import (
11
+ CallBlock,
12
+ ModelEntry,
13
+ ReplyContent,
14
+ TextSegment,
15
+ ThoughtSegment,
16
+ UsageStats,
17
+ )
18
+ from rlmagent.harness.provider.wire import (
19
+ CallChunkEvent,
20
+ CallCloseEvent,
21
+ CallOpenEvent,
22
+ CompletionCause,
23
+ ContentChunkEvent,
24
+ ContentCloseEvent,
25
+ ContentOpenEvent,
26
+ FaultCause,
27
+ ReasoningChunkEvent,
28
+ ReasoningCloseEvent,
29
+ ReasoningOpenEvent,
30
+ StreamCloseEvent,
31
+ StreamFaultEvent,
32
+ StreamOpenEvent,
33
+ WireEvent,
34
+ )
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Phase enum
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ class Phase(Enum):
42
+ """Lifecycle phase of the streaming response."""
43
+
44
+ PENDING = auto()
45
+ ACTIVE = auto()
46
+ SEALED = auto()
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Content accumulator
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ @dataclass(slots=True)
55
+ class _ContentAccumulator:
56
+ """Mutable tracker for one in-progress content block."""
57
+
58
+ slot: int
59
+ variety: str # "text" | "thinking" | "tool_use"
60
+ chunks: list[str] = field(default_factory=list)
61
+ tool_id: str = ""
62
+ tool_name: str = ""
63
+ # Opaque proof the API attaches to thinking (or the whole payload of a
64
+ # redacted block). It must be sent back verbatim on the next request.
65
+ signature: str | None = None
66
+
67
+ @property
68
+ def assembled(self) -> str:
69
+ """Return the concatenated content gathered so far."""
70
+ return "".join(self.chunks)
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Helpers
75
+ # ---------------------------------------------------------------------------
76
+
77
+
78
+ def _map_stop_reason(reason: str) -> str:
79
+ """Translate an Anthropic stop reason to rlm-agent's halt vocabulary."""
80
+ match reason:
81
+ case "end_turn" | "stop_sequence":
82
+ return "stop"
83
+ case "max_tokens":
84
+ return "length"
85
+ case "tool_use":
86
+ return "toolUse"
87
+ case _:
88
+ return "stop"
89
+
90
+
91
+ def _safe_json_args(text: str) -> dict[str, Any]:
92
+ """Parse tool-call input JSON, falling back to an empty dict."""
93
+ if not text or not text.strip():
94
+ return {}
95
+ try:
96
+ obj = json.loads(text)
97
+ return obj if isinstance(obj, dict) else {}
98
+ except (json.JSONDecodeError, ValueError):
99
+ return {}
100
+
101
+
102
+ def _tally_usage(
103
+ *,
104
+ input_tokens: int = 0,
105
+ output_tokens: int = 0,
106
+ cache_read: int = 0,
107
+ cache_write: int = 0,
108
+ ) -> UsageStats:
109
+ """Build a ``UsageStats`` from Anthropic token counts."""
110
+ return UsageStats(
111
+ total_tokens=input_tokens + output_tokens,
112
+ input=input_tokens,
113
+ output=output_tokens,
114
+ cache_read=cache_read,
115
+ cache_write=cache_write,
116
+ )
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # State machine
121
+ # ---------------------------------------------------------------------------
122
+
123
+
124
+ class ResponseMachine:
125
+ """Stateful parser that ingests Anthropic SSE events and emits wire events."""
126
+
127
+ def __init__(self, *, model: str, provider: str) -> None:
128
+ self._model = model
129
+ self._provider = provider
130
+ self._phase = Phase.PENDING
131
+
132
+ # Response-level metadata
133
+ self._response_id: str | None = None
134
+ self._response_model: str | None = None
135
+ self._halt: str = "stop"
136
+
137
+ # Token accounting
138
+ self._input_tokens = 0
139
+ self._output_tokens = 0
140
+ self._cache_read = 0
141
+ self._cache_write = 0
142
+
143
+ # Completed content blocks
144
+ self._thoughts: list[ThoughtSegment] = []
145
+ self._texts: list[TextSegment] = []
146
+ self._calls: list[CallBlock] = []
147
+
148
+ # Active block (at most one at a time)
149
+ self._accumulator: _ContentAccumulator | None = None
150
+
151
+ # --- snapshot -----------------------------------------------------------
152
+
153
+ def _snapshot(self) -> ModelEntry:
154
+ """Build a ``ModelEntry`` reflecting the current accumulated state."""
155
+ blocks: list[ReplyContent] = []
156
+ blocks.extend(self._thoughts)
157
+ blocks.extend(self._texts)
158
+ blocks.extend(self._calls)
159
+
160
+ # Include partial content from the active block
161
+ if self._accumulator is not None:
162
+ body = self._accumulator.assembled
163
+ if self._accumulator.variety == "thinking" and body:
164
+ blocks.append(ThoughtSegment(thinking=body))
165
+ elif self._accumulator.variety == "text" and body:
166
+ blocks.append(TextSegment(text=body))
167
+
168
+ return ModelEntry(
169
+ model=self._response_model or self._model,
170
+ provider=self._provider,
171
+ api="messages",
172
+ response_id=self._response_id,
173
+ response_model=self._response_model,
174
+ content=blocks,
175
+ stop_reason=self._halt, # type: ignore[arg-type]
176
+ usage=_tally_usage(
177
+ input_tokens=self._input_tokens,
178
+ output_tokens=self._output_tokens,
179
+ cache_read=self._cache_read,
180
+ cache_write=self._cache_write,
181
+ ),
182
+ )
183
+
184
+ # --- public interface ---------------------------------------------------
185
+
186
+ def ingest(self, kind: str, data: dict[str, Any]) -> list[WireEvent]:
187
+ """Process one SSE event and return any resulting wire events."""
188
+ match kind:
189
+ case "message_start":
190
+ return self._on_message_start(data)
191
+ case "content_block_start":
192
+ return self._on_block_open(data)
193
+ case "content_block_delta":
194
+ return self._on_block_delta(data)
195
+ case "content_block_stop":
196
+ return self._on_block_close(data)
197
+ case "message_delta":
198
+ return self._on_message_delta(data)
199
+ case "message_stop":
200
+ return self.seal()
201
+ case "ping":
202
+ return []
203
+ case "error":
204
+ return self._on_error(data)
205
+ case _:
206
+ return []
207
+
208
+ def seal(self) -> list[WireEvent]:
209
+ """Emit the terminal event if the machine has not already been sealed."""
210
+ if self._phase == Phase.SEALED:
211
+ return []
212
+ self._phase = Phase.SEALED
213
+
214
+ message = self._snapshot()
215
+
216
+ if self._halt in ("error", "aborted"):
217
+ cause: FaultCause = "aborted" if self._halt == "aborted" else "error"
218
+ return [StreamFaultEvent(reason=cause, error=message)]
219
+
220
+ reason: CompletionCause = "stop"
221
+ if self._halt == "length":
222
+ reason = "length"
223
+ elif self._halt == "toolUse":
224
+ reason = "toolUse"
225
+ return [StreamCloseEvent(reason=reason, message=message)]
226
+
227
+ # --- event handlers -----------------------------------------------------
228
+
229
+ def _on_message_start(self, data: dict[str, Any]) -> list[WireEvent]:
230
+ msg = data.get("message", {})
231
+ self._response_id = msg.get("id")
232
+ self._response_model = msg.get("model")
233
+
234
+ usage = msg.get("usage", {})
235
+ self._input_tokens = usage.get("input_tokens", 0)
236
+ self._cache_read = usage.get("cache_read_input_tokens", 0)
237
+ self._cache_write = usage.get("cache_creation_input_tokens", 0)
238
+
239
+ self._phase = Phase.ACTIVE
240
+ return [StreamOpenEvent(partial=self._snapshot())]
241
+
242
+ def _on_block_open(self, data: dict[str, Any]) -> list[WireEvent]:
243
+ block = data.get("content_block", {})
244
+ slot = data.get("index", 0)
245
+ variety = block.get("type", "text")
246
+
247
+ if variety == "tool_use":
248
+ self._accumulator = _ContentAccumulator(
249
+ slot=slot,
250
+ variety="tool_use",
251
+ tool_id=block.get("id", ""),
252
+ tool_name=block.get("name", ""),
253
+ )
254
+ return [CallOpenEvent(content_index=slot, partial=self._snapshot())]
255
+
256
+ if variety == "thinking":
257
+ self._accumulator = _ContentAccumulator(
258
+ slot=slot, variety="thinking", signature=block.get("signature") or None,
259
+ )
260
+ return [ReasoningOpenEvent(content_index=slot, partial=self._snapshot())]
261
+
262
+ if variety == "redacted_thinking":
263
+ self._accumulator = _ContentAccumulator(
264
+ slot=slot, variety="redacted_thinking", signature=block.get("data") or None,
265
+ )
266
+ return [ReasoningOpenEvent(content_index=slot, partial=self._snapshot())]
267
+
268
+ # Default: text
269
+ self._accumulator = _ContentAccumulator(slot=slot, variety="text")
270
+ return [ContentOpenEvent(content_index=slot, partial=self._snapshot())]
271
+
272
+ def _on_block_delta(self, data: dict[str, Any]) -> list[WireEvent]:
273
+ if self._accumulator is None:
274
+ return []
275
+
276
+ delta = data.get("delta", {})
277
+ delta_type = delta.get("type", "")
278
+ slot = data.get("index", self._accumulator.slot)
279
+
280
+ if delta_type == "text_delta":
281
+ fragment = delta.get("text", "")
282
+ if fragment:
283
+ self._accumulator.chunks.append(fragment)
284
+ return [ContentChunkEvent(
285
+ content_index=slot, delta=fragment, partial=self._snapshot(),
286
+ )]
287
+
288
+ elif delta_type == "thinking_delta":
289
+ fragment = delta.get("thinking", "")
290
+ if fragment:
291
+ self._accumulator.chunks.append(fragment)
292
+ return [ReasoningChunkEvent(
293
+ content_index=slot, delta=fragment, partial=self._snapshot(),
294
+ )]
295
+
296
+ elif delta_type == "signature_delta":
297
+ self._accumulator.signature = (
298
+ self._accumulator.signature or ""
299
+ ) + delta.get("signature", "")
300
+ return []
301
+
302
+ elif delta_type == "input_json_delta":
303
+ fragment = delta.get("partial_json", "")
304
+ if fragment:
305
+ self._accumulator.chunks.append(fragment)
306
+ return [CallChunkEvent(
307
+ content_index=slot, delta=fragment, partial=self._snapshot(),
308
+ )]
309
+
310
+ return []
311
+
312
+ def _on_block_close(self, data: dict[str, Any]) -> list[WireEvent]:
313
+ acc = self._accumulator
314
+ if acc is None:
315
+ return []
316
+ self._accumulator = None
317
+ slot = data.get("index", acc.slot)
318
+ body = acc.assembled
319
+
320
+ if acc.variety == "text":
321
+ self._texts.append(TextSegment(text=body))
322
+ return [ContentCloseEvent(
323
+ content_index=slot, content=body, partial=self._snapshot(),
324
+ )]
325
+
326
+ if acc.variety in ("thinking", "redacted_thinking"):
327
+ self._thoughts.append(ThoughtSegment(
328
+ thinking=body,
329
+ redacted=acc.variety == "redacted_thinking",
330
+ thinking_signature=acc.signature,
331
+ ))
332
+ return [ReasoningCloseEvent(
333
+ content_index=slot, content=body, partial=self._snapshot(),
334
+ )]
335
+
336
+ if acc.variety == "tool_use":
337
+ args = _safe_json_args(body)
338
+ call = CallBlock(id=acc.tool_id, name=acc.tool_name, arguments=args)
339
+ self._calls.append(call)
340
+ return [CallCloseEvent(
341
+ content_index=slot, tool_call=call, partial=self._snapshot(),
342
+ )]
343
+
344
+ return []
345
+
346
+ def _on_message_delta(self, data: dict[str, Any]) -> list[WireEvent]:
347
+ delta = data.get("delta", {})
348
+ stop = delta.get("stop_reason")
349
+ if stop:
350
+ self._halt = _map_stop_reason(stop)
351
+
352
+ usage = data.get("usage", {})
353
+ out = usage.get("output_tokens")
354
+ if out is not None:
355
+ self._output_tokens = int(out)
356
+ return []
357
+
358
+ def _on_error(self, data: dict[str, Any]) -> list[WireEvent]:
359
+ was_pending = self._phase == Phase.PENDING
360
+ self._phase = Phase.SEALED
361
+ self._halt = "error"
362
+
363
+ error_obj = data.get("error", {})
364
+ msg = error_obj.get("message", str(data))
365
+ entry = ModelEntry(
366
+ model=self._response_model or self._model,
367
+ provider=self._provider,
368
+ api="messages",
369
+ content=[],
370
+ stop_reason="error",
371
+ error_message=msg,
372
+ )
373
+
374
+ events: list[WireEvent] = []
375
+ if was_pending:
376
+ events.append(StreamOpenEvent(partial=entry))
377
+ events.append(StreamFaultEvent(reason="error", error=entry))
378
+ return events
@@ -0,0 +1 @@
1
+ """Internal implementation package for the OpenAI-compatible provider adapter."""