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,397 @@
1
+ """Stateful reusable agent harness built on the portable loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
7
+ from contextlib import suppress
8
+ from dataclasses import dataclass, field
9
+ from inspect import isawaitable
10
+ from typing import Literal
11
+
12
+ from rlmagent.harness.contracts.stream import (
13
+ AgentEvent,
14
+ MessageEndEvent,
15
+ MessageStartEvent,
16
+ )
17
+ from rlmagent.harness.contracts.tooling import ToolSpec
18
+ from rlmagent.harness.contracts.transcript import (
19
+ ExtensionEntry,
20
+ HumanEntry,
21
+ InputContent,
22
+ ModelEntry,
23
+ ToolOutcomeEntry,
24
+ TranscriptEntry,
25
+ )
26
+ from rlmagent.harness.contracts.values import JValue
27
+ from rlmagent.harness.engine import PostToolHook, PreToolHook, run_agent_loop
28
+ from rlmagent.harness.provider.base import ModelProvider
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Public type aliases
32
+ # ---------------------------------------------------------------------------
33
+
34
+ EventCallback = Callable[[AgentEvent], Awaitable[None] | None]
35
+ """Signature for event subscribers — sync or async callables."""
36
+
37
+ DrainPolicy = Literal["one_at_a_time", "all"]
38
+ """How many queued messages to drain per opportunity."""
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Cancellation
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ class ManualCancelFlag:
47
+ """Concrete cancellation token triggered by external code."""
48
+
49
+ __slots__ = ("_triggered",)
50
+
51
+ def __init__(self) -> None:
52
+ self._triggered = False
53
+
54
+ def cancel(self) -> None:
55
+ """Signal cancellation."""
56
+ self._triggered = True
57
+
58
+ def is_cancelled(self) -> bool:
59
+ """Check whether cancellation was signalled."""
60
+ return self._triggered
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Queue snapshots
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class QueueShiftEvent:
70
+ """Lightweight snapshot emitted whenever a queue changes."""
71
+
72
+ steering: tuple[InputContent, ...] = ()
73
+ follow_up: tuple[InputContent, ...] = ()
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class PendingBatch:
78
+ """Frozen view of messages waiting in both queues."""
79
+
80
+ steering: tuple[TranscriptEntry, ...] = ()
81
+ follow_up: tuple[TranscriptEntry, ...] = ()
82
+
83
+ @property
84
+ def count(self) -> int:
85
+ """Total number of pending messages across both queues."""
86
+ return len(self.steering) + len(self.follow_up)
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Configuration
91
+ # ---------------------------------------------------------------------------
92
+
93
+
94
+ @dataclass(slots=True)
95
+ class RuntimeConfig:
96
+ """Mutable bag of knobs for a ``RuntimeHarness``."""
97
+
98
+ provider: ModelProvider
99
+ model: str
100
+ system: str
101
+ tools: list[ToolSpec] = field(default_factory=list)
102
+ max_turns: int | None = None
103
+ queue_mode: DrainPolicy = "one_at_a_time"
104
+ before_tool_call: PreToolHook | None = None
105
+ after_tool_call: PostToolHook | None = None
106
+ # Stable conversation id forwarded to every model request for cache routing.
107
+ cache_key: str | None = None
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Harness
112
+ # ---------------------------------------------------------------------------
113
+
114
+
115
+ class RuntimeHarness:
116
+ """Reusable stateful agent brain."""
117
+
118
+ __slots__ = (
119
+ "_cfg",
120
+ "_entries",
121
+ "_watchers",
122
+ "_flag",
123
+ "_running",
124
+ "_steer_q",
125
+ "_followup_q",
126
+ )
127
+
128
+ def __init__(
129
+ self,
130
+ config: RuntimeConfig,
131
+ *,
132
+ messages: Sequence[TranscriptEntry] = (),
133
+ ) -> None:
134
+ self._cfg = config
135
+ self._entries: list[TranscriptEntry] = list(messages)
136
+ self._watchers: list[EventCallback] = []
137
+ self._flag: ManualCancelFlag | None = None
138
+ self._running = False
139
+ self._steer_q: deque[TranscriptEntry] = deque()
140
+ self._followup_q: deque[TranscriptEntry] = deque()
141
+
142
+ # -- read-only views ----------------------------------------------------
143
+
144
+ @property
145
+ def transcript(self) -> tuple[TranscriptEntry, ...]:
146
+ """Immutable snapshot of the current transcript."""
147
+ return tuple(self._entries)
148
+
149
+ @property
150
+ def settings(self) -> RuntimeConfig:
151
+ """The harness configuration object."""
152
+ return self._cfg
153
+
154
+ @property
155
+ def active(self) -> bool:
156
+ """Whether a submit or resume is in progress."""
157
+ return self._running
158
+
159
+ @property
160
+ def pending(self) -> PendingBatch:
161
+ """Frozen view of both queues."""
162
+ return PendingBatch(
163
+ steering=tuple(self._steer_q),
164
+ follow_up=tuple(self._followup_q),
165
+ )
166
+
167
+ @property
168
+ def pending_count(self) -> int:
169
+ """Total queued message count."""
170
+ return self.pending.count
171
+
172
+ # -- transcript mutation ------------------------------------------------
173
+
174
+ def has_pending(self) -> bool:
175
+ """True when either queue holds at least one message."""
176
+ return bool(self._steer_q or self._followup_q)
177
+
178
+ def push_message(self, message: TranscriptEntry) -> None:
179
+ """Append an entry directly (e.g. when restoring session state)."""
180
+ self._entries.append(message)
181
+
182
+ def set_messages(self, messages: Sequence[TranscriptEntry]) -> None:
183
+ """Replace the entire transcript (e.g. after context reconstruction)."""
184
+ self._entries = list(messages)
185
+
186
+ # -- subscriptions ------------------------------------------------------
187
+
188
+ def on_event(self, listener: EventCallback) -> Callable[[], None]:
189
+ """Register a subscriber; returns an unsubscribe handle."""
190
+ self._watchers.append(listener)
191
+
192
+ def _detach() -> None:
193
+ with suppress(ValueError):
194
+ self._watchers.remove(listener)
195
+
196
+ return _detach
197
+
198
+ # -- cancellation -------------------------------------------------------
199
+
200
+ def abort(self) -> None:
201
+ """Cancel the active loop, if any."""
202
+ if self._flag is not None:
203
+ self._flag.cancel()
204
+
205
+ # -- queue operations ---------------------------------------------------
206
+
207
+ def inject(self, content: str) -> QueueShiftEvent:
208
+ """Convenience: queue a steering ``HumanEntry`` from plain text."""
209
+ return self.inject_message(HumanEntry(content=content))
210
+
211
+ def inject_message(self, message: TranscriptEntry) -> QueueShiftEvent:
212
+ """Queue a message to be injected after the current turn/tool batch."""
213
+ self._steer_q.append(message)
214
+ return self.build_queue_event()
215
+
216
+ def enqueue(self, content: str) -> QueueShiftEvent:
217
+ """Convenience: queue a follow-up ``HumanEntry`` from plain text."""
218
+ return self.enqueue_message(HumanEntry(content=content))
219
+
220
+ def enqueue_message(self, message: TranscriptEntry) -> QueueShiftEvent:
221
+ """Queue a message to be sent when the active run would otherwise stop."""
222
+ self._followup_q.append(message)
223
+ return self.build_queue_event()
224
+
225
+ def flush_queues(self) -> PendingBatch:
226
+ """Drain and return everything from both queues."""
227
+ snap = self.pending
228
+ self._steer_q.clear()
229
+ self._followup_q.clear()
230
+ return snap
231
+
232
+ def retract_enqueued(self) -> TranscriptEntry | None:
233
+ """Pop the newest follow-up message, or ``None``."""
234
+ return self._followup_q.pop() if self._followup_q else None
235
+
236
+ def retract_injected(self) -> TranscriptEntry | None:
237
+ """Pop the newest steering message, or ``None``."""
238
+ return self._steer_q.pop() if self._steer_q else None
239
+
240
+ def build_queue_event(self) -> QueueShiftEvent:
241
+ """Build a ``QueueShiftEvent`` reflecting current queue contents."""
242
+ return QueueShiftEvent(
243
+ steering=tuple(m.content for m in self._steer_q),
244
+ follow_up=tuple(m.content for m in self._followup_q),
245
+ )
246
+
247
+ # -- run lifecycle ------------------------------------------------------
248
+
249
+ def submit(
250
+ self,
251
+ content: str,
252
+ *,
253
+ custom_type: str | None = None,
254
+ details: dict[str, JValue] | None = None,
255
+ ) -> AsyncIterator[AgentEvent]:
256
+ """Append a user message and start the agent loop."""
257
+ self._assert_idle()
258
+ self._heal_dangling_calls()
259
+ self._running = True
260
+
261
+ entry: TranscriptEntry
262
+ if custom_type is not None:
263
+ entry = ExtensionEntry(
264
+ custom_type=custom_type,
265
+ content=content,
266
+ details=details,
267
+ )
268
+ else:
269
+ entry = HumanEntry(content=content)
270
+ self._entries.append(entry)
271
+ return self._drive(prompt_entry=entry)
272
+
273
+ def resume(self) -> AsyncIterator[AgentEvent]:
274
+ """Continue without appending a new user message."""
275
+ self._assert_idle()
276
+ self._heal_dangling_calls()
277
+ self._running = True
278
+ return self._drive()
279
+
280
+ # -- public repair ------------------------------------------------------
281
+
282
+ def patch_interrupted_calls(self) -> int:
283
+ """Repair dangling tool calls and return number of patches applied."""
284
+ before = len(self._entries)
285
+ self._heal_dangling_calls()
286
+ return len(self._entries) - before
287
+
288
+ # -- private machinery --------------------------------------------------
289
+
290
+ async def _drive(
291
+ self,
292
+ *,
293
+ prompt_entry: TranscriptEntry | None = None,
294
+ ) -> AsyncIterator[AgentEvent]:
295
+ """Core generator: run the loop, broadcast events, emit prompt echo."""
296
+ token = ManualCancelFlag()
297
+ self._flag = token
298
+ pending_prompt = prompt_entry
299
+ try:
300
+ async for event in run_agent_loop(
301
+ provider=self._cfg.provider,
302
+ model=self._cfg.model,
303
+ system=self._cfg.system,
304
+ messages=self._entries,
305
+ tools=self._cfg.tools,
306
+ max_turns=self._cfg.max_turns,
307
+ signal=token,
308
+ get_steering_messages=self._pop_steered,
309
+ get_follow_up_messages=self._pop_followups,
310
+ before_tool_call=self._cfg.before_tool_call,
311
+ after_tool_call=self._cfg.after_tool_call,
312
+ cache_key=self._cfg.cache_key,
313
+ ):
314
+ await self._fan_out(event)
315
+ yield event
316
+ # Echo the prompt as start/end pair on the first turn boundary
317
+ if pending_prompt is not None and event.type == "turn_start":
318
+ for echo in (
319
+ MessageStartEvent(message=pending_prompt),
320
+ MessageEndEvent(message=pending_prompt),
321
+ ):
322
+ await self._fan_out(echo)
323
+ yield echo
324
+ pending_prompt = None
325
+ finally:
326
+ if token.is_cancelled():
327
+ self._heal_dangling_calls()
328
+ if self._flag is token:
329
+ self._flag = None
330
+ self._running = False
331
+
332
+ async def _fan_out(self, event: AgentEvent) -> None:
333
+ """Notify every subscriber, awaiting async ones."""
334
+ for watcher in list(self._watchers):
335
+ rv = watcher(event)
336
+ if isawaitable(rv):
337
+ await rv
338
+
339
+ def _assert_idle(self) -> None:
340
+ """Raise if another run is already active."""
341
+ if self._running:
342
+ raise RuntimeError(
343
+ "RuntimeHarness is already running; "
344
+ "use inject() or enqueue() to queue messages."
345
+ )
346
+
347
+ def _pop_steered(self) -> tuple[TranscriptEntry, ...]:
348
+ """Drain the steering queue according to the configured policy."""
349
+ return self._drain(self._steer_q)
350
+
351
+ def _pop_followups(self) -> tuple[TranscriptEntry, ...]:
352
+ """Drain the follow-up queue according to the configured policy."""
353
+ return self._drain(self._followup_q)
354
+
355
+ def _drain(self, q: deque[TranscriptEntry]) -> tuple[TranscriptEntry, ...]:
356
+ """Pull messages from *q* respecting ``queue_mode``."""
357
+ if not q:
358
+ return ()
359
+ if self._cfg.queue_mode == "all":
360
+ batch = tuple(q)
361
+ q.clear()
362
+ return batch
363
+ return (q.popleft(),)
364
+
365
+ def _heal_dangling_calls(self) -> None:
366
+ """Ensure every tool call in the transcript has a matching result."""
367
+ answered: set[str] = {
368
+ e.tool_call_id
369
+ for e in self._entries
370
+ if isinstance(e, ToolOutcomeEntry)
371
+ }
372
+ for entry in tuple(self._entries):
373
+ if not isinstance(entry, ModelEntry):
374
+ continue
375
+ for call in entry.tool_calls:
376
+ if call.id in answered:
377
+ continue
378
+ answered.add(call.id)
379
+ self._entries.append(
380
+ ToolOutcomeEntry(
381
+ tool_call_id=call.id,
382
+ tool_name=call.name,
383
+ content="Tool call interrupted by user",
384
+ is_error=True,
385
+ )
386
+ )
387
+
388
+
389
+ __all__ = [
390
+ "DrainPolicy",
391
+ "EventCallback",
392
+ "ManualCancelFlag",
393
+ "PendingBatch",
394
+ "QueueShiftEvent",
395
+ "RuntimeConfig",
396
+ "RuntimeHarness",
397
+ ]