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.
- rlmagent/__init__.py +1 -0
- rlmagent/__main__.py +8 -0
- rlmagent/acp/__init__.py +1 -0
- rlmagent/acp/runtime.py +71 -0
- rlmagent/acp/server.py +598 -0
- rlmagent/acp/updates.py +128 -0
- rlmagent/agentnet.py +237 -0
- rlmagent/agents/__init__.py +1 -0
- rlmagent/agents/approval.py +90 -0
- rlmagent/agents/bridge.py +109 -0
- rlmagent/agents/budget.py +212 -0
- rlmagent/agents/child_handles.py +46 -0
- rlmagent/agents/child_mailbox.py +30 -0
- rlmagent/agents/kernel_api.py +156 -0
- rlmagent/agents/output.py +34 -0
- rlmagent/agents/patch.py +107 -0
- rlmagent/agents/prompts.py +156 -0
- rlmagent/agents/trace.py +290 -0
- rlmagent/agents/tree.py +682 -0
- rlmagent/agents/wasm_api.py +105 -0
- rlmagent/agents/workspace.py +357 -0
- rlmagent/autopilot.py +447 -0
- rlmagent/backend.py +946 -0
- rlmagent/backend_context.py +74 -0
- rlmagent/cli/__init__.py +5 -0
- rlmagent/cli/config_cmd.py +177 -0
- rlmagent/cli/health.py +156 -0
- rlmagent/cli/main.py +1194 -0
- rlmagent/cli/update.py +188 -0
- rlmagent/clipboard.py +63 -0
- rlmagent/config/__init__.py +37 -0
- rlmagent/config/config.py +297 -0
- rlmagent/config/loader.py +119 -0
- rlmagent/config/prompts.py +124 -0
- rlmagent/config/secure_input.py +213 -0
- rlmagent/config/store.py +275 -0
- rlmagent/context/__init__.py +47 -0
- rlmagent/context/budget.py +557 -0
- rlmagent/contract.py +311 -0
- rlmagent/contract_run.py +232 -0
- rlmagent/conversation.py +2547 -0
- rlmagent/cron.py +77 -0
- rlmagent/directives.py +893 -0
- rlmagent/discovery.py +356 -0
- rlmagent/execution_log.py +66 -0
- rlmagent/factory.py +189 -0
- rlmagent/factory_graph.py +413 -0
- rlmagent/harness/__init__.py +63 -0
- rlmagent/harness/contracts/__init__.py +1 -0
- rlmagent/harness/contracts/stream.py +129 -0
- rlmagent/harness/contracts/tooling.py +127 -0
- rlmagent/harness/contracts/transcript/__init__.py +68 -0
- rlmagent/harness/contracts/transcript/_base.py +29 -0
- rlmagent/harness/contracts/transcript/diagnostics.py +52 -0
- rlmagent/harness/contracts/transcript/entries.py +180 -0
- rlmagent/harness/contracts/transcript/segments.py +64 -0
- rlmagent/harness/contracts/values.py +6 -0
- rlmagent/harness/driver.py +397 -0
- rlmagent/harness/engine.py +489 -0
- rlmagent/harness/mending.py +153 -0
- rlmagent/harness/provider/__init__.py +1 -0
- rlmagent/harness/provider/base.py +27 -0
- rlmagent/harness/provider/wire.py +189 -0
- rlmagent/harness/py.typed +0 -0
- rlmagent/harness/session/__init__.py +5 -0
- rlmagent/harness/session/index.py +218 -0
- rlmagent/harness/session/lease.py +66 -0
- rlmagent/harness/session/records.py +118 -0
- rlmagent/harness/session/replay.py +165 -0
- rlmagent/harness/session/store.py +155 -0
- rlmagent/harness/session/summary.py +46 -0
- rlmagent/hints.py +47 -0
- rlmagent/history.py +92 -0
- rlmagent/hooks/__init__.py +1 -0
- rlmagent/hooks/hooks.py +1 -0
- rlmagent/instructions.py +157 -0
- rlmagent/job_runner.py +117 -0
- rlmagent/jobs.py +365 -0
- rlmagent/kept_agents.py +397 -0
- rlmagent/kernel/__init__.py +5 -0
- rlmagent/kernel/session.py +205 -0
- rlmagent/kernel/wasm.py +169 -0
- rlmagent/logs.py +208 -0
- rlmagent/managed_release.py +368 -0
- rlmagent/mcp.py +411 -0
- rlmagent/mcp_oauth.py +213 -0
- rlmagent/model/__init__.py +29 -0
- rlmagent/model/_claude/__init__.py +1 -0
- rlmagent/model/_claude/courier.py +117 -0
- rlmagent/model/_claude/emitter.py +378 -0
- rlmagent/model/_oai/__init__.py +1 -0
- rlmagent/model/_oai/helpers.py +256 -0
- rlmagent/model/_oai/normalize.py +263 -0
- rlmagent/model/_oai/parsers.py +317 -0
- rlmagent/model/_oai/payloads.py +226 -0
- rlmagent/model/_oai/transport.py +127 -0
- rlmagent/model/claude.py +552 -0
- rlmagent/model/copilot.py +142 -0
- rlmagent/model/correlation.py +21 -0
- rlmagent/model/deadline.py +19 -0
- rlmagent/model/discovery.py +181 -0
- rlmagent/model/external/__init__.py +11 -0
- rlmagent/model/external/acp.py +202 -0
- rlmagent/model/external/acp_install.py +109 -0
- rlmagent/model/external/cli.py +248 -0
- rlmagent/model/external/textcalls.py +57 -0
- rlmagent/model/failover.py +129 -0
- rlmagent/model/google.py +199 -0
- rlmagent/model/limits.py +46 -0
- rlmagent/model/oai_compatible.py +414 -0
- rlmagent/model/preferences.py +56 -0
- rlmagent/model/py.typed +0 -0
- rlmagent/model/registered_oauth.py +195 -0
- rlmagent/model/scripted.py +55 -0
- rlmagent/model/settings.py +271 -0
- rlmagent/model/subscription.py +398 -0
- rlmagent/model/token_bounds.py +15 -0
- rlmagent/model/transport/__init__.py +1 -0
- rlmagent/model/transport/backoff.py +87 -0
- rlmagent/model/transport/client.py +87 -0
- rlmagent/model/transport/faults.py +65 -0
- rlmagent/model/transport/responses_socket.py +223 -0
- rlmagent/model/vertex.py +93 -0
- rlmagent/network.py +123 -0
- rlmagent/packages.py +246 -0
- rlmagent/planning.py +84 -0
- rlmagent/playbook/__init__.py +1 -0
- rlmagent/playbook/apply.py +106 -0
- rlmagent/playbook/digest.py +63 -0
- rlmagent/playbook/improver.py +272 -0
- rlmagent/playbook/plan.py +99 -0
- rlmagent/playbook/store.py +67 -0
- rlmagent/plugins.py +327 -0
- rlmagent/policy.py +105 -0
- rlmagent/prompt_refs.py +57 -0
- rlmagent/prompts.py +331 -0
- rlmagent/prune.py +60 -0
- rlmagent/py.typed +0 -0
- rlmagent/reasoning.py +135 -0
- rlmagent/refinement/__init__.py +1 -0
- rlmagent/refinement/controller.py +448 -0
- rlmagent/refinement/engine.py +220 -0
- rlmagent/refinement/evidence.py +131 -0
- rlmagent/refinement/scoped.py +90 -0
- rlmagent/refinement/store.py +193 -0
- rlmagent/refresh.py +202 -0
- rlmagent/routing.py +52 -0
- rlmagent/runtime.py +575 -0
- rlmagent/safety.py +267 -0
- rlmagent/session_html.py +57 -0
- rlmagent/session_import.py +52 -0
- rlmagent/session_share.py +55 -0
- rlmagent/sessions.py +249 -0
- rlmagent/shell_sandbox.py +88 -0
- rlmagent/skillset.py +320 -0
- rlmagent/steering.py +148 -0
- rlmagent/stops.py +62 -0
- rlmagent/subscriptions.py +219 -0
- rlmagent/tasks.py +98 -0
- rlmagent/toolhost.py +310 -0
- rlmagent/tools/__init__.py +56 -0
- rlmagent/tools/_shared.py +210 -0
- rlmagent/tools/files.py +462 -0
- rlmagent/tools/kernel.py +93 -0
- rlmagent/tools/patch_tool.py +68 -0
- rlmagent/tools/search.py +188 -0
- rlmagent/trust.py +383 -0
- rlmagent/tui/__init__.py +1 -0
- rlmagent/tui/adapter.py +506 -0
- rlmagent/tui/app.py +711 -0
- rlmagent/tui/approval_screen.py +57 -0
- rlmagent/tui/choice_screen.py +47 -0
- rlmagent/tui/completions.py +116 -0
- rlmagent/tui/components.py +25 -0
- rlmagent/tui/composer.py +40 -0
- rlmagent/tui/factory_view.py +86 -0
- rlmagent/tui/inspect.py +193 -0
- rlmagent/tui/messages.py +215 -0
- rlmagent/tui/model_screen.py +307 -0
- rlmagent/tui/prompt.py +98 -0
- rlmagent/tui/question_screen.py +52 -0
- rlmagent/tui/replay.py +141 -0
- rlmagent/tui/rlm_agent.tcss +405 -0
- rlmagent/tui/search_box.py +91 -0
- rlmagent/tui/sidebar.py +182 -0
- rlmagent/tui/status.py +49 -0
- rlmagent/tui/steps.py +283 -0
- rlmagent/tui/suggest.py +778 -0
- rlmagent/tui/transcript.py +172 -0
- rlmagent/tui/trust_dialog.py +58 -0
- rlmagent/tui/worker_attach.py +141 -0
- rlmagent/ui/__init__.py +1 -0
- rlmagent/ui/render.py +211 -0
- rlmagent/undo.py +109 -0
- rlmagent/utils/__init__.py +1 -0
- rlmagent/websearch.py +75 -0
- rlmagent/worker.py +1275 -0
- rlmagent/worker_service.py +203 -0
- rlmness_cli-0.2.0.dist-info/METADATA +882 -0
- rlmness_cli-0.2.0.dist-info/RECORD +203 -0
- rlmness_cli-0.2.0.dist-info/WHEEL +4 -0
- rlmness_cli-0.2.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|