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
rlmagent/agents/tree.py
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"""Every agent in one run: the root, its sub-agents, and theirs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
import itertools
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from time import time
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
from rlmness import Allowance, AllowanceSpent
|
|
17
|
+
|
|
18
|
+
from rlmagent.agents.budget import BudgetedProvider
|
|
19
|
+
from rlmagent.agents.child_handles import checked_name
|
|
20
|
+
from rlmagent.agents.child_mailbox import notes_metadata, read_note, record_notes
|
|
21
|
+
from rlmagent.agents.trace import TracedProvider, Tracer
|
|
22
|
+
from rlmagent.agents.bridge import BridgeServer, kernel_env
|
|
23
|
+
from rlmagent.agents.kernel_api import KERNEL_API
|
|
24
|
+
from rlmagent.agents.approval import Approver, Refused
|
|
25
|
+
from rlmagent.agents.wasm_api import wasm_api
|
|
26
|
+
from rlmagent.agents.workspace import Workspace
|
|
27
|
+
from rlmagent.kernel import KernelSession
|
|
28
|
+
from rlmagent.kernel.wasm import WasmKernel
|
|
29
|
+
from rlmagent.tools import build_tool_registry
|
|
30
|
+
from rlmagent.model.external import is_external, parse_model, with_external
|
|
31
|
+
from rlmagent.harness.contracts.tooling import ToolSpec
|
|
32
|
+
|
|
33
|
+
NUDGE = (
|
|
34
|
+
"You have not called FINAL. When your work is done, call FINAL(value) in the "
|
|
35
|
+
"python tool with the result the agent that started you asked for."
|
|
36
|
+
)
|
|
37
|
+
NEVER_FINAL = "[this sub-agent never called FINAL; this is its last reply]"
|
|
38
|
+
_ENDED = ("done", "failed", "cancelled")
|
|
39
|
+
_SETTLED = (*_ENDED, "idle")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class AgentNode:
|
|
44
|
+
id: str
|
|
45
|
+
depth: int
|
|
46
|
+
parent_id: str | None
|
|
47
|
+
task: str = ""
|
|
48
|
+
context: object = None
|
|
49
|
+
final_value: object = None
|
|
50
|
+
final_given: bool = False
|
|
51
|
+
final_note: str = ""
|
|
52
|
+
role_text: str = ""
|
|
53
|
+
model: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ChildRecord:
|
|
58
|
+
node: AgentNode
|
|
59
|
+
keep: bool = False
|
|
60
|
+
status: str = "starting"
|
|
61
|
+
task: asyncio.Task | None = None
|
|
62
|
+
session: object = None
|
|
63
|
+
value: object = None
|
|
64
|
+
error: str | None = None
|
|
65
|
+
holds_slot: bool = False
|
|
66
|
+
inbox: asyncio.Queue = field(default_factory=asyncio.Queue)
|
|
67
|
+
outbox: list = field(default_factory=list)
|
|
68
|
+
notes: list = field(default_factory=list)
|
|
69
|
+
changed: asyncio.Event = field(default_factory=asyncio.Event)
|
|
70
|
+
confirm: tuple[int, int] | None = None
|
|
71
|
+
name: str | None = None
|
|
72
|
+
on_status: Callable[[str], None] | None = None
|
|
73
|
+
|
|
74
|
+
def set(self, status: str) -> None:
|
|
75
|
+
self.status = status
|
|
76
|
+
self.changed.set()
|
|
77
|
+
self.changed = asyncio.Event()
|
|
78
|
+
if self.on_status is not None:
|
|
79
|
+
self.on_status(status)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
GUARD_MIN_CHARS = 5000
|
|
83
|
+
GUARD_RATIO = 0.6
|
|
84
|
+
|
|
85
|
+
CONFIRM = (
|
|
86
|
+
"Before starting, check this hand-off. Your CONTEXT holds {child:,} characters; the agent "
|
|
87
|
+
"that started you holds {parent:,}, so you got {pct}% of it. Handing on nearly everything "
|
|
88
|
+
"usually means the work of narrowing it down was skipped.\n"
|
|
89
|
+
"Answer with YES or NO as the very first word. YES: this hand-off makes sense as it is "
|
|
90
|
+
"(for example, the same step applied to one piece). NO: the starting agent should narrow "
|
|
91
|
+
"the data before handing it on. After a NO, say why in one sentence and add a short "
|
|
92
|
+
"```python cell that agent could run on its own data to narrow it (pick the relevant part, "
|
|
93
|
+
"search, split) for this task.")
|
|
94
|
+
CONFIRM_BATCH = (
|
|
95
|
+
"Before these {n} sub-agents start together, check the hand-off. Your data holds "
|
|
96
|
+
"{parent:,} characters, and each would get:\n{lines}\nFor {flagged} of them that is most of "
|
|
97
|
+
"your data, barely narrowed.\n"
|
|
98
|
+
"Answer with YES or NO as the very first word (YES lets all of them start), then one "
|
|
99
|
+
"sentence on why.")
|
|
100
|
+
REJECTED = (
|
|
101
|
+
"Sub-agent not started: it would have received {pct}% of your data, barely narrowed. "
|
|
102
|
+
"Narrow it in your own kernel and hand on only the part the task needs. "
|
|
103
|
+
"The check said:\n{reason}")
|
|
104
|
+
BATCH_REJECTED = (
|
|
105
|
+
"gather_rlm not started: some jobs would have received most of your data, barely "
|
|
106
|
+
"narrowed. Narrow each one in your own kernel first. The check said:\n{reason}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _size(value: object) -> int:
|
|
110
|
+
return len(value) if isinstance(value, str) else len(json.dumps(value, default=repr))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AgentTree:
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
*,
|
|
117
|
+
provider,
|
|
118
|
+
provider_name: str,
|
|
119
|
+
model: str,
|
|
120
|
+
cwd: str,
|
|
121
|
+
allowance: Allowance,
|
|
122
|
+
sessions_dir: Path | None,
|
|
123
|
+
system_for: Callable[[AgentNode, list[ToolSpec]], str],
|
|
124
|
+
first_message_for: Callable[[AgentNode], str],
|
|
125
|
+
cell_timeout: float = 300.0,
|
|
126
|
+
max_agents: int = 50,
|
|
127
|
+
tracer: Tracer | None = None,
|
|
128
|
+
kernel: str = "local",
|
|
129
|
+
approver: Approver | None = None,
|
|
130
|
+
network: bool = False,
|
|
131
|
+
guard: bool = True,
|
|
132
|
+
restore_kernel: bool = False,
|
|
133
|
+
) -> None:
|
|
134
|
+
self.guard = guard
|
|
135
|
+
self.restore_kernel = restore_kernel
|
|
136
|
+
self.kernel_kind = kernel
|
|
137
|
+
self.network = network
|
|
138
|
+
self.approver = approver or Approver("deny")
|
|
139
|
+
self.provider = BudgetedProvider(with_external(provider), allowance)
|
|
140
|
+
self.provider_name = provider_name
|
|
141
|
+
self.model = model
|
|
142
|
+
self.cwd = cwd
|
|
143
|
+
self.allowance = allowance
|
|
144
|
+
self.sessions_dir = sessions_dir
|
|
145
|
+
self.system_for = system_for
|
|
146
|
+
self.first_message_for = first_message_for
|
|
147
|
+
self.cell_timeout = cell_timeout
|
|
148
|
+
self.max_agents = max_agents
|
|
149
|
+
self.tracer = tracer or Tracer(None)
|
|
150
|
+
self.tool_filter = None
|
|
151
|
+
self.improver = None
|
|
152
|
+
self._finished = False
|
|
153
|
+
self.records: dict[str, ChildRecord] = {}
|
|
154
|
+
self._kids: dict[str, list[str]] = {}
|
|
155
|
+
self._agents_started = 0
|
|
156
|
+
self.nodes: dict[str, AgentNode] = {}
|
|
157
|
+
self.kernels: dict[str, KernelSession] = {}
|
|
158
|
+
self.sessions: list = []
|
|
159
|
+
self._children: dict[str, list[str]] = {}
|
|
160
|
+
self._session_of: dict[str, object] = {}
|
|
161
|
+
self._bridge = BridgeServer(self._handle)
|
|
162
|
+
self._live = asyncio.Semaphore(max(1, allowance.max_live))
|
|
163
|
+
self._ids = itertools.count(1)
|
|
164
|
+
self._root_id: str | None = None
|
|
165
|
+
self.listeners = []
|
|
166
|
+
self.event_path: Path | None = None
|
|
167
|
+
self._event_prefix = uuid4().hex
|
|
168
|
+
self._reported_root = False
|
|
169
|
+
|
|
170
|
+
async def start(self) -> None:
|
|
171
|
+
await self._bridge.start()
|
|
172
|
+
|
|
173
|
+
def provider_for(self, agent_id: str) -> TracedProvider:
|
|
174
|
+
return TracedProvider(self, agent_id)
|
|
175
|
+
|
|
176
|
+
def use_provider(self, provider, provider_name: str) -> BudgetedProvider:
|
|
177
|
+
"""Switch every agent, from the next call on, to `provider`, under the same budget."""
|
|
178
|
+
self.provider = BudgetedProvider(with_external(provider), self.allowance)
|
|
179
|
+
self.provider_name = provider_name
|
|
180
|
+
return self.provider
|
|
181
|
+
|
|
182
|
+
def attach(self, agent_id: str, session) -> None:
|
|
183
|
+
"""Record sub-agents this agent starts in `session`'s saved file."""
|
|
184
|
+
self._session_of[agent_id] = session
|
|
185
|
+
if agent_id == self.root_id():
|
|
186
|
+
self.session = session
|
|
187
|
+
|
|
188
|
+
def root_id(self) -> str:
|
|
189
|
+
if self._root_id is None:
|
|
190
|
+
self.root_kernel()
|
|
191
|
+
return self._root_id
|
|
192
|
+
|
|
193
|
+
def _can_delegate(self, node: AgentNode) -> bool:
|
|
194
|
+
return node.depth < self.allowance.max_depth
|
|
195
|
+
|
|
196
|
+
def _kernel_for(self, node: AgentNode) -> KernelSession:
|
|
197
|
+
if self.kernel_kind == "wasm":
|
|
198
|
+
return self._sandbox_for(node)
|
|
199
|
+
if not self._bridge.port:
|
|
200
|
+
raise RuntimeError("start() the agent tree before creating kernels")
|
|
201
|
+
env = kernel_env(
|
|
202
|
+
self._bridge.port,
|
|
203
|
+
self._bridge.issue(node.id),
|
|
204
|
+
node.id,
|
|
205
|
+
can_delegate=self._can_delegate(node),
|
|
206
|
+
is_child=node.parent_id is not None,
|
|
207
|
+
)
|
|
208
|
+
kernel = KernelSession(
|
|
209
|
+
cwd=self.cwd, env=env, startup_code=KERNEL_API, timeout=self.cell_timeout
|
|
210
|
+
)
|
|
211
|
+
self.kernels[node.id] = kernel
|
|
212
|
+
return kernel
|
|
213
|
+
|
|
214
|
+
def _sandbox_for(self, node: AgentNode) -> WasmKernel:
|
|
215
|
+
"""Give each recursive agent a private interpreter in the shared Node host."""
|
|
216
|
+
workspace = Workspace(self.cwd, self.approver)
|
|
217
|
+
workspace.policy = getattr(self, "policy", None)
|
|
218
|
+
functions = workspace.functions()
|
|
219
|
+
if workspace.policy is not None:
|
|
220
|
+
functions = {name: fn for name, fn in functions.items()
|
|
221
|
+
if workspace.policy.allows_tool(name)}
|
|
222
|
+
|
|
223
|
+
async def call(op, args):
|
|
224
|
+
return await self._handle(node.id, op, args)
|
|
225
|
+
|
|
226
|
+
async def final(value):
|
|
227
|
+
await self._handle(node.id, "final", {"value": value, "note": ""})
|
|
228
|
+
|
|
229
|
+
kernel = WasmKernel(
|
|
230
|
+
cwd=self.cwd, bridges={"_rlmagent": call, **functions},
|
|
231
|
+
startup_code=wasm_api(self._can_delegate(node), node.parent_id is not None,
|
|
232
|
+
network=self.network),
|
|
233
|
+
timeout=self.cell_timeout, on_final=final, restore=self.restore_kernel,
|
|
234
|
+
)
|
|
235
|
+
self.kernels[node.id] = kernel
|
|
236
|
+
return kernel
|
|
237
|
+
|
|
238
|
+
def root_kernel(self) -> KernelSession:
|
|
239
|
+
if self._root_id is None:
|
|
240
|
+
node = AgentNode(id="root", depth=0, parent_id=None)
|
|
241
|
+
self.nodes[node.id] = node
|
|
242
|
+
self._root_id = node.id
|
|
243
|
+
self._kernel_for(node)
|
|
244
|
+
return self.kernels[self._root_id]
|
|
245
|
+
|
|
246
|
+
async def _handle(self, agent_id: str, op: str, args: dict) -> object:
|
|
247
|
+
node = self.nodes.get(agent_id)
|
|
248
|
+
if node is None:
|
|
249
|
+
raise RuntimeError(f"unknown agent {agent_id!r}")
|
|
250
|
+
policy = getattr(self, "policy", None)
|
|
251
|
+
if op in ("guidance", "improve", "playbook") and policy is not None and not policy.allows_tool(op):
|
|
252
|
+
raise Refused(f"{op} is disabled by the tool policy")
|
|
253
|
+
if op == "context":
|
|
254
|
+
return node.context
|
|
255
|
+
if op == "final":
|
|
256
|
+
node.final_value = args.get("value")
|
|
257
|
+
node.final_note = str(args.get("note") or "")
|
|
258
|
+
node.final_given = True
|
|
259
|
+
return None
|
|
260
|
+
if op == "capabilities":
|
|
261
|
+
from rlmagent.backend import limits_of
|
|
262
|
+
|
|
263
|
+
return limits_of(self)
|
|
264
|
+
if op == "guidance":
|
|
265
|
+
refinement = getattr(self, "refinement", None)
|
|
266
|
+
if refinement is None:
|
|
267
|
+
raise RuntimeError("No refined guidance is attached to this run")
|
|
268
|
+
return refinement.read_guidance(args.get("id"))
|
|
269
|
+
if op in ("improve", "playbook"):
|
|
270
|
+
improver = self._improver_of(node)
|
|
271
|
+
if improver is None:
|
|
272
|
+
raise RuntimeError("the playbook is not available in this run")
|
|
273
|
+
if op == "playbook":
|
|
274
|
+
if args.get("id"):
|
|
275
|
+
return improver.entry(str(args["id"]))
|
|
276
|
+
return [{"id": e.id, "kind": e.kind, "title": e.title} for e in improver.entries()]
|
|
277
|
+
improver.request(args.get("instructions"), bool(args.get("shared")))
|
|
278
|
+
return "Noted: the playbook will be updated when this turn ends."
|
|
279
|
+
if op == "tell_parent":
|
|
280
|
+
rec = self.records.get(agent_id)
|
|
281
|
+
if rec is None:
|
|
282
|
+
raise RuntimeError("only sub-agents have a parent")
|
|
283
|
+
text = str(args.get("text", ""))
|
|
284
|
+
messages = [*rec.outbox, text]
|
|
285
|
+
rec.notes = record_notes(rec.notes, rec.outbox, messages)
|
|
286
|
+
rec.outbox = messages
|
|
287
|
+
self._child_event("child_progress", agent_id, text=text[:500])
|
|
288
|
+
return None
|
|
289
|
+
if op == "spawn":
|
|
290
|
+
return self.spawn(
|
|
291
|
+
agent_id, str(args.get("task", "")), args.get("context"), bool(args.get("keep")),
|
|
292
|
+
role=args.get("role"), model=args.get("model"),
|
|
293
|
+
)
|
|
294
|
+
if op == "children":
|
|
295
|
+
return list(self._kids.get(agent_id, []))
|
|
296
|
+
if op == "child_handle":
|
|
297
|
+
action = args.get("action")
|
|
298
|
+
if action in ("name", "find"):
|
|
299
|
+
name = checked_name(args.get("name"))
|
|
300
|
+
match = next((key for key in self._kids.get(agent_id, ())
|
|
301
|
+
if self.records[key].name == name
|
|
302
|
+
and self.records[key].status != "cancelled"), None)
|
|
303
|
+
if action == "find":
|
|
304
|
+
if match is None:
|
|
305
|
+
raise ValueError(f"no child named {name!r}")
|
|
306
|
+
return {"id": match}
|
|
307
|
+
if match is not None and match != args.get("id"):
|
|
308
|
+
raise ValueError(f"child name {name!r} is already in use")
|
|
309
|
+
rec = self._own(agent_id, str(args.get("id")))
|
|
310
|
+
if rec.status == "cancelled":
|
|
311
|
+
raise ValueError("child is cancelled")
|
|
312
|
+
ledger = getattr(getattr(self, "session", None), "steering", None)
|
|
313
|
+
if action == "messages":
|
|
314
|
+
return notes_metadata(rec.notes, args.get("new_only", True))
|
|
315
|
+
if action == "read_message":
|
|
316
|
+
rec.notes, note = read_note(rec.notes, args.get("identity"))
|
|
317
|
+
return note
|
|
318
|
+
if action == "steer":
|
|
319
|
+
if ledger is None:
|
|
320
|
+
raise ValueError("child steering requires an attached session")
|
|
321
|
+
return ledger.accept(args.get("text"), args.get("message_id"), rec.node.id, once=True,
|
|
322
|
+
allow_new=rec.status in ("starting", "running"))
|
|
323
|
+
if action == "name":
|
|
324
|
+
rec.name = name
|
|
325
|
+
self._child_event("child_named", rec.node.id, name=name)
|
|
326
|
+
elif action != "info":
|
|
327
|
+
raise ValueError("invalid child handle operation")
|
|
328
|
+
return {"id": rec.node.id, "name": rec.name, "status": rec.status, "keep": rec.keep,
|
|
329
|
+
"queued": rec.inbox.qsize(),
|
|
330
|
+
"steering": ledger.receipts(target=rec.node.id) if ledger is not None else [],
|
|
331
|
+
"usage": self.tracer.child_usage(rec.node.id, compatibility=True)}
|
|
332
|
+
if op == "rlm":
|
|
333
|
+
child = self.records[self.spawn(agent_id, str(args.get("task", "")), args.get("context"),
|
|
334
|
+
role=args.get("role"), model=args.get("model"))]
|
|
335
|
+
async with self._waiting_on_children(node):
|
|
336
|
+
try:
|
|
337
|
+
return await self._result(child, None)
|
|
338
|
+
except asyncio.CancelledError:
|
|
339
|
+
await self._cancel(child)
|
|
340
|
+
raise
|
|
341
|
+
if op == "gather":
|
|
342
|
+
jobs = [(str(task), context) for task, context in args.get("jobs", [])]
|
|
343
|
+
children = []
|
|
344
|
+
try:
|
|
345
|
+
await self._confirm_batch(node, jobs)
|
|
346
|
+
for task, context in jobs:
|
|
347
|
+
children.append(self.records[self.spawn(agent_id, task, context, confirm=False)])
|
|
348
|
+
async with self._waiting_on_children(node):
|
|
349
|
+
return await self._gather(children)
|
|
350
|
+
except BaseException:
|
|
351
|
+
for child in children:
|
|
352
|
+
await self._cancel(child)
|
|
353
|
+
raise
|
|
354
|
+
rec = self._own(agent_id, str(args.get("id")))
|
|
355
|
+
if op == "status":
|
|
356
|
+
return rec.status
|
|
357
|
+
if op == "messages":
|
|
358
|
+
out, rec.outbox = rec.outbox, []
|
|
359
|
+
rec.notes = record_notes(rec.notes, out, [])
|
|
360
|
+
return out
|
|
361
|
+
if op == "cancel":
|
|
362
|
+
await self._cancel(rec)
|
|
363
|
+
return None
|
|
364
|
+
if op == "send":
|
|
365
|
+
await self._send(rec, str(args.get("text", "")))
|
|
366
|
+
return None
|
|
367
|
+
if op == "result":
|
|
368
|
+
async with self._waiting_on_children(node):
|
|
369
|
+
return await self._result(rec, args.get("timeout"))
|
|
370
|
+
raise RuntimeError(f"unknown request {op!r}")
|
|
371
|
+
|
|
372
|
+
def _improver_of(self, node: AgentNode):
|
|
373
|
+
if node.parent_id is None:
|
|
374
|
+
return self.improver
|
|
375
|
+
rec = self.records.get(node.id)
|
|
376
|
+
return getattr(rec.session, "improver", None) if rec else None
|
|
377
|
+
|
|
378
|
+
def _role(self, role: str) -> str:
|
|
379
|
+
try:
|
|
380
|
+
entry = self.improver.entry(role) if self.improver else None
|
|
381
|
+
except KeyError:
|
|
382
|
+
entry = None
|
|
383
|
+
if entry is None or entry["kind"] != "role":
|
|
384
|
+
raise RuntimeError(f"no playbook role named {role!r}")
|
|
385
|
+
return f"## Your role: {entry['title']}\n{entry['content']}"
|
|
386
|
+
|
|
387
|
+
def _own(self, agent_id: str, child_id: str) -> ChildRecord:
|
|
388
|
+
rec = self.records.get(child_id)
|
|
389
|
+
if rec is None or rec.node.parent_id != agent_id:
|
|
390
|
+
raise RuntimeError(f"{child_id} is not your sub-agent")
|
|
391
|
+
return rec
|
|
392
|
+
|
|
393
|
+
async def _gather(self, children: list[ChildRecord]) -> list:
|
|
394
|
+
try:
|
|
395
|
+
async with asyncio.TaskGroup() as group:
|
|
396
|
+
waits = [group.create_task(self._result(c, None)) for c in children]
|
|
397
|
+
except* Exception as failed:
|
|
398
|
+
raise failed.exceptions[0] from None
|
|
399
|
+
return [w.result() for w in waits]
|
|
400
|
+
|
|
401
|
+
@contextlib.asynccontextmanager
|
|
402
|
+
async def _waiting_on_children(self, node: AgentNode):
|
|
403
|
+
"""Pause the agent's cell timeout and free its live slot while it awaits sub-agents."""
|
|
404
|
+
with self.kernels[node.id].waiting_outside():
|
|
405
|
+
rec = self.records.get(node.id)
|
|
406
|
+
if rec is None or not rec.holds_slot:
|
|
407
|
+
yield
|
|
408
|
+
return
|
|
409
|
+
self._release(rec)
|
|
410
|
+
try:
|
|
411
|
+
yield
|
|
412
|
+
finally:
|
|
413
|
+
await self._acquire(rec)
|
|
414
|
+
|
|
415
|
+
async def _acquire(self, rec: ChildRecord) -> None:
|
|
416
|
+
await self._live.acquire()
|
|
417
|
+
rec.holds_slot = True
|
|
418
|
+
|
|
419
|
+
def _release(self, rec: ChildRecord) -> None:
|
|
420
|
+
if rec.holds_slot:
|
|
421
|
+
rec.holds_slot = False
|
|
422
|
+
self._live.release()
|
|
423
|
+
|
|
424
|
+
def _child_event(self, kind: str, agent_id: str, **fields) -> None:
|
|
425
|
+
run_id = f"{self._event_prefix}/{agent_id}"
|
|
426
|
+
event = {"timestamp": time(), "kind": kind, "run_id": run_id, **fields}
|
|
427
|
+
if agent_id != self._root_id:
|
|
428
|
+
event["child_id"] = run_id
|
|
429
|
+
if self.event_path is not None:
|
|
430
|
+
self.event_path.parent.mkdir(parents=True, exist_ok=True)
|
|
431
|
+
with self.event_path.open("a", encoding="utf-8") as stream:
|
|
432
|
+
stream.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
433
|
+
for listener in tuple(self.listeners):
|
|
434
|
+
listener(event)
|
|
435
|
+
|
|
436
|
+
def spawn(self, parent_id: str, task: str, context: object, keep: bool = False,
|
|
437
|
+
role: str | None = None, model: str | None = None, confirm: bool = True) -> str:
|
|
438
|
+
parent = self.nodes[parent_id]
|
|
439
|
+
contract_model = getattr(self, "contract_model", None)
|
|
440
|
+
if contract_model is not None and model is not None and model != contract_model:
|
|
441
|
+
raise RuntimeError("sub-agents must use the model specified by the runtime contract")
|
|
442
|
+
role_text = self._role(role) if role else ""
|
|
443
|
+
if is_external(model):
|
|
444
|
+
parse_model(model)
|
|
445
|
+
if not self._can_delegate(parent):
|
|
446
|
+
raise RuntimeError("sub-agents are not available at this depth")
|
|
447
|
+
if self._agents_started >= self.max_agents:
|
|
448
|
+
raise RuntimeError(f"the limit of {self.max_agents} sub-agents started is reached")
|
|
449
|
+
self._agents_started += 1
|
|
450
|
+
node = AgentNode(
|
|
451
|
+
id=f"{parent_id}.{next(self._ids)}",
|
|
452
|
+
depth=parent.depth + 1,
|
|
453
|
+
parent_id=parent_id,
|
|
454
|
+
task=task,
|
|
455
|
+
context=context,
|
|
456
|
+
role_text=role_text,
|
|
457
|
+
model=model or parent.model,
|
|
458
|
+
)
|
|
459
|
+
self.nodes[node.id] = node
|
|
460
|
+
rec = ChildRecord(node=node, keep=keep,
|
|
461
|
+
confirm=self._barely_compressed(parent, context) if confirm else None)
|
|
462
|
+
self.records[node.id] = rec
|
|
463
|
+
self._kids.setdefault(parent_id, []).append(node.id)
|
|
464
|
+
self.tracer.spawned(parent_id, node.id)
|
|
465
|
+
if not self._reported_root:
|
|
466
|
+
root = self.nodes[self._root_id]
|
|
467
|
+
self._child_event("run_started", root.id, depth=0, model=root.model)
|
|
468
|
+
self._reported_root = True
|
|
469
|
+
self._child_event("run_started", node.id, parent_run_id=f"{self._event_prefix}/{parent_id}",
|
|
470
|
+
depth=node.depth, model=node.model)
|
|
471
|
+
rec.on_status = lambda status: self._child_event("child_status", node.id, status=status)
|
|
472
|
+
rec.on_status(rec.status)
|
|
473
|
+
rec.task = asyncio.get_running_loop().create_task(self._run(rec))
|
|
474
|
+
return node.id
|
|
475
|
+
|
|
476
|
+
async def run_child(self, parent_id: str, task: str, context: object) -> object:
|
|
477
|
+
return await self._result(self.records[self.spawn(parent_id, task, context)], None)
|
|
478
|
+
|
|
479
|
+
async def _result(self, rec: ChildRecord, timeout: float | None) -> object:
|
|
480
|
+
async def settled() -> object:
|
|
481
|
+
while rec.status not in _SETTLED:
|
|
482
|
+
await rec.changed.wait()
|
|
483
|
+
if rec.status == "failed":
|
|
484
|
+
raise RuntimeError(rec.error)
|
|
485
|
+
if rec.status == "cancelled":
|
|
486
|
+
raise RuntimeError(f"{rec.node.id} was cancelled")
|
|
487
|
+
self.tracer.returned(rec.node.parent_id, rec.node.id)
|
|
488
|
+
return rec.value
|
|
489
|
+
|
|
490
|
+
if timeout is None:
|
|
491
|
+
return await settled()
|
|
492
|
+
try:
|
|
493
|
+
return await asyncio.wait_for(settled(), float(timeout))
|
|
494
|
+
except (asyncio.TimeoutError, TimeoutError):
|
|
495
|
+
raise TimeoutError(f"{rec.node.id} is still {rec.status}") from None
|
|
496
|
+
|
|
497
|
+
async def _cancel(self, rec: ChildRecord) -> None:
|
|
498
|
+
if rec.task is None:
|
|
499
|
+
return
|
|
500
|
+
if rec.status not in _ENDED:
|
|
501
|
+
rec.task.cancel()
|
|
502
|
+
# A finished agent may still be shutting its kernel down; let that end.
|
|
503
|
+
await asyncio.gather(rec.task, return_exceptions=True)
|
|
504
|
+
|
|
505
|
+
async def _send(self, rec: ChildRecord, text: str) -> None:
|
|
506
|
+
if rec.status in _ENDED:
|
|
507
|
+
raise RuntimeError(f"{rec.node.id} has finished; start a new sub-agent")
|
|
508
|
+
if rec.status == "idle":
|
|
509
|
+
rec.set("starting")
|
|
510
|
+
rec.inbox.put_nowait(text)
|
|
511
|
+
elif rec.session is not None:
|
|
512
|
+
rec.session.inject(text)
|
|
513
|
+
else:
|
|
514
|
+
rec.inbox.put_nowait(text)
|
|
515
|
+
|
|
516
|
+
async def _run(self, rec: ChildRecord) -> None:
|
|
517
|
+
from rlmagent.conversation import CodingSession
|
|
518
|
+
|
|
519
|
+
node = rec.node
|
|
520
|
+
ledger = getattr(getattr(self, "session", None), "steering", None)
|
|
521
|
+
kernel = None
|
|
522
|
+
try:
|
|
523
|
+
await self._acquire(rec)
|
|
524
|
+
rec.set("running")
|
|
525
|
+
if rec.confirm is not None:
|
|
526
|
+
await self._confirm(node, *rec.confirm)
|
|
527
|
+
kernel = self._kernel_for(node)
|
|
528
|
+
tools = (self.tool_filter or build_tool_registry)(kernel)
|
|
529
|
+
session = await CodingSession.create(
|
|
530
|
+
provider=self.provider_for(node.id),
|
|
531
|
+
provider_name=self.provider_name,
|
|
532
|
+
model=node.model or self.model,
|
|
533
|
+
system=self.system_for(node, tools) + (f"\n\n{node.role_text}" if node.role_text else ""),
|
|
534
|
+
tools=tools,
|
|
535
|
+
sessions_dir=self.sessions_dir,
|
|
536
|
+
cwd=self.cwd,
|
|
537
|
+
)
|
|
538
|
+
rec.session = session
|
|
539
|
+
if self.improver is not None:
|
|
540
|
+
from rlmagent.playbook.improver import Improver
|
|
541
|
+
|
|
542
|
+
own = self.sessions_dir / f"{session.session_id}.playbook.json" if self.sessions_dir else None
|
|
543
|
+
session.improver = Improver(session, local_path=own,
|
|
544
|
+
shared_path=self.improver.paths["global"])
|
|
545
|
+
session.improver.auto = False
|
|
546
|
+
session.on_compacted = lambda: self.tracer.compacted(node.id)
|
|
547
|
+
await session.set_name(f"sub-agent {node.id}: {node.task[:60]}")
|
|
548
|
+
self.sessions.append(session)
|
|
549
|
+
self._children.setdefault(node.parent_id, []).append(session.session_id)
|
|
550
|
+
self.attach(node.id, session)
|
|
551
|
+
parent_session = self._session_of.get(node.parent_id)
|
|
552
|
+
if parent_session is not None:
|
|
553
|
+
await parent_session.note_sub_agent(session.session_id)
|
|
554
|
+
message = self.first_message_for(node)
|
|
555
|
+
while True:
|
|
556
|
+
rec.value = await self._round(session, node, message)
|
|
557
|
+
if ledger is not None:
|
|
558
|
+
ledger.expire_child(node.id)
|
|
559
|
+
if not rec.keep:
|
|
560
|
+
rec.set("done")
|
|
561
|
+
return
|
|
562
|
+
self._release(rec)
|
|
563
|
+
rec.set("idle")
|
|
564
|
+
message = await rec.inbox.get()
|
|
565
|
+
await self._acquire(rec)
|
|
566
|
+
node.final_given = False
|
|
567
|
+
rec.set("running")
|
|
568
|
+
except asyncio.CancelledError:
|
|
569
|
+
rec.set("cancelled")
|
|
570
|
+
except Exception as exc:
|
|
571
|
+
rec.error = str(exc) or type(exc).__name__
|
|
572
|
+
rec.set("failed")
|
|
573
|
+
finally:
|
|
574
|
+
self._release(rec)
|
|
575
|
+
for child_id in self._kids.get(node.id, []):
|
|
576
|
+
await self._cancel(self.records[child_id])
|
|
577
|
+
if kernel is not None:
|
|
578
|
+
if "RLM_AGENT_BRIDGE_TOKEN" in kernel.env:
|
|
579
|
+
self._bridge.revoke(kernel.env["RLM_AGENT_BRIDGE_TOKEN"])
|
|
580
|
+
await kernel.shutdown()
|
|
581
|
+
if ledger is not None and rec.status in ("failed", "cancelled"):
|
|
582
|
+
ledger.expire_child(node.id)
|
|
583
|
+
|
|
584
|
+
# -- delegation guard: a sub-agent handed most of its parent's context confirms first --
|
|
585
|
+
|
|
586
|
+
def _barely_compressed(self, parent: AgentNode, context: object) -> tuple[int, int] | None:
|
|
587
|
+
if not self.guard or parent.context is None:
|
|
588
|
+
return None
|
|
589
|
+
parent_chars, child_chars = _size(parent.context), _size(context)
|
|
590
|
+
if parent_chars >= GUARD_MIN_CHARS and child_chars >= GUARD_RATIO * parent_chars:
|
|
591
|
+
return child_chars, parent_chars
|
|
592
|
+
return None
|
|
593
|
+
|
|
594
|
+
async def _judge(self, node: AgentNode, question: str) -> tuple[bool, str]:
|
|
595
|
+
"""One tool-less call with the sub-agent's own instructions; only an explicit NO refuses."""
|
|
596
|
+
from rlmagent.conversation import CodingSession
|
|
597
|
+
|
|
598
|
+
session = await CodingSession.create(
|
|
599
|
+
provider=self.provider_for(node.id), provider_name=self.provider_name,
|
|
600
|
+
model=node.model or self.model, system=self.system_for(node, []), tools=[],
|
|
601
|
+
sessions_dir=None, cwd=self.cwd)
|
|
602
|
+
try:
|
|
603
|
+
async for _event in session.submit(question):
|
|
604
|
+
pass
|
|
605
|
+
reply = _last_text(session).strip()
|
|
606
|
+
finally:
|
|
607
|
+
await session.shutdown()
|
|
608
|
+
if self.provider.refusal is not None:
|
|
609
|
+
raise AllowanceSpent(self.provider.refusal)
|
|
610
|
+
word = re.search(r"[A-Za-z]+", reply)
|
|
611
|
+
return (word is None or word.group().upper() != "NO"), reply or "(no reason given)"
|
|
612
|
+
|
|
613
|
+
async def _confirm(self, node: AgentNode, child_chars: int, parent_chars: int) -> None:
|
|
614
|
+
pct = round(100 * child_chars / max(1, parent_chars))
|
|
615
|
+
ok, reason = await self._judge(node, self.first_message_for(node) + "\n\n" + CONFIRM.format(
|
|
616
|
+
child=child_chars, pct=pct, parent=parent_chars))
|
|
617
|
+
if not ok:
|
|
618
|
+
raise RuntimeError(REJECTED.format(pct=pct, reason=reason))
|
|
619
|
+
|
|
620
|
+
async def _confirm_batch(self, parent: AgentNode, jobs: list[tuple[str, object]]) -> None:
|
|
621
|
+
"""One check for a whole gather_rlm, and only if some job gets most of the parent's context."""
|
|
622
|
+
flagged = [self._barely_compressed(parent, context) for _, context in jobs]
|
|
623
|
+
if not any(flagged):
|
|
624
|
+
return
|
|
625
|
+
parent_chars = _size(parent.context)
|
|
626
|
+
lines = "\n".join(
|
|
627
|
+
f" [{i}] {_size(c):,} chars ({round(100 * _size(c) / max(1, parent_chars))}% of the "
|
|
628
|
+
f"parent's context): {task[:140]}" for i, (task, c) in enumerate(jobs, 1))
|
|
629
|
+
ok, reason = await self._judge(parent, CONFIRM_BATCH.format(
|
|
630
|
+
n=len(jobs), parent=parent_chars, lines=lines, flagged=sum(1 for f in flagged if f)))
|
|
631
|
+
if not ok:
|
|
632
|
+
raise RuntimeError(BATCH_REJECTED.format(reason=reason))
|
|
633
|
+
|
|
634
|
+
async def _round(self, session, node: AgentNode, message: str) -> object:
|
|
635
|
+
for _ in range(2):
|
|
636
|
+
async for _event in session.submit(message):
|
|
637
|
+
pass
|
|
638
|
+
if self.provider.refusal is not None:
|
|
639
|
+
raise AllowanceSpent(self.provider.refusal)
|
|
640
|
+
if node.final_given:
|
|
641
|
+
if node.final_note:
|
|
642
|
+
return f"{node.final_value}\n[{node.final_note}]"
|
|
643
|
+
return node.final_value
|
|
644
|
+
message = NUDGE
|
|
645
|
+
return f"{_last_text(session)}\n{NEVER_FINAL}"
|
|
646
|
+
|
|
647
|
+
def child_session_ids(self, parent_id: str) -> list[str]:
|
|
648
|
+
"""Saved-session ids of the sub-agents an agent started, in order."""
|
|
649
|
+
return list(self._children.get(parent_id, []))
|
|
650
|
+
|
|
651
|
+
async def close(self) -> None:
|
|
652
|
+
for rec in list(self.records.values()):
|
|
653
|
+
await self._cancel(rec)
|
|
654
|
+
for kernel in list(self.kernels.values()):
|
|
655
|
+
await kernel.shutdown()
|
|
656
|
+
await self._bridge.close()
|
|
657
|
+
self.finish_trace()
|
|
658
|
+
|
|
659
|
+
def finish_trace(self, complete: bool = True) -> dict | None:
|
|
660
|
+
if self._finished:
|
|
661
|
+
return None
|
|
662
|
+
self._finished = True
|
|
663
|
+
root = self._session_of.get(self._root_id or "root")
|
|
664
|
+
agents = [{"agent": "root", "parent": None, "task": None, "status": "done",
|
|
665
|
+
"session": getattr(root, "session_id", None)}]
|
|
666
|
+
agents += [
|
|
667
|
+
{"agent": r.node.id, "parent": r.node.parent_id, "task": r.node.task,
|
|
668
|
+
"status": r.status, "session": getattr(r.session, "session_id", None)}
|
|
669
|
+
for r in self.records.values()
|
|
670
|
+
]
|
|
671
|
+
agents += [{"agent": key, "parent": item.get("parent_run_id"),
|
|
672
|
+
"task": item.get("instruction"), "status": item.get("status"), "session": None}
|
|
673
|
+
for key, item in getattr(self, "backend_agents", {}).items()
|
|
674
|
+
if item.get("depth", 0) > 0]
|
|
675
|
+
return self.tracer.finish(agents, self.provider.refusal, complete)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _last_text(session) -> str:
|
|
679
|
+
for entry in reversed(session.transcript):
|
|
680
|
+
if getattr(entry, "role", None) == "assistant":
|
|
681
|
+
return entry.text
|
|
682
|
+
return ""
|