bareagent-cli 0.1.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.
- bareagent/__init__.py +10 -0
- bareagent/concurrency/__init__.py +6 -0
- bareagent/concurrency/background.py +97 -0
- bareagent/concurrency/notification.py +61 -0
- bareagent/concurrency/scheduler.py +136 -0
- bareagent/config.toml +299 -0
- bareagent/core/__init__.py +1 -0
- bareagent/core/config_paths.py +49 -0
- bareagent/core/context.py +127 -0
- bareagent/core/fileutil.py +103 -0
- bareagent/core/goal.py +214 -0
- bareagent/core/handlers/__init__.py +1 -0
- bareagent/core/handlers/bash.py +79 -0
- bareagent/core/handlers/file_edit.py +47 -0
- bareagent/core/handlers/file_read.py +270 -0
- bareagent/core/handlers/file_write.py +34 -0
- bareagent/core/handlers/glob_search.py +30 -0
- bareagent/core/handlers/goal.py +60 -0
- bareagent/core/handlers/grep_search.py +52 -0
- bareagent/core/handlers/memory.py +71 -0
- bareagent/core/handlers/plan.py +106 -0
- bareagent/core/handlers/search_utils.py +77 -0
- bareagent/core/handlers/skill.py +87 -0
- bareagent/core/handlers/subagent_send.py +70 -0
- bareagent/core/handlers/web_fetch.py +126 -0
- bareagent/core/handlers/web_search.py +165 -0
- bareagent/core/handlers/workflow.py +190 -0
- bareagent/core/loop.py +535 -0
- bareagent/core/retry.py +131 -0
- bareagent/core/sandbox.py +27 -0
- bareagent/core/schema.py +21 -0
- bareagent/core/tools.py +779 -0
- bareagent/core/workflow.py +517 -0
- bareagent/core/workflow_registry.py +219 -0
- bareagent/debug/__init__.py +0 -0
- bareagent/debug/interaction_log.py +263 -0
- bareagent/debug/viewer.html +1750 -0
- bareagent/debug/web_viewer.py +157 -0
- bareagent/hooks/__init__.py +32 -0
- bareagent/hooks/config.py +118 -0
- bareagent/hooks/engine.py +197 -0
- bareagent/hooks/errors.py +14 -0
- bareagent/hooks/events.py +22 -0
- bareagent/lsp/__init__.py +63 -0
- bareagent/lsp/config.py +134 -0
- bareagent/lsp/coord.py +118 -0
- bareagent/lsp/diagnostics.py +240 -0
- bareagent/lsp/errors.py +24 -0
- bareagent/lsp/manager.py +866 -0
- bareagent/lsp/tools.py +629 -0
- bareagent/lsp/workspace_edit.py +305 -0
- bareagent/main.py +4205 -0
- bareagent/mcp/__init__.py +69 -0
- bareagent/mcp/_sse.py +69 -0
- bareagent/mcp/client.py +341 -0
- bareagent/mcp/config.py +169 -0
- bareagent/mcp/errors.py +32 -0
- bareagent/mcp/manager.py +318 -0
- bareagent/mcp/protocol.py +187 -0
- bareagent/mcp/registry.py +557 -0
- bareagent/mcp/transport/__init__.py +15 -0
- bareagent/mcp/transport/base.py +149 -0
- bareagent/mcp/transport/http_legacy.py +192 -0
- bareagent/mcp/transport/http_streamable.py +217 -0
- bareagent/mcp/transport/stdio.py +202 -0
- bareagent/memory/__init__.py +1 -0
- bareagent/memory/compact.py +203 -0
- bareagent/memory/conversation_io.py +226 -0
- bareagent/memory/embedding.py +194 -0
- bareagent/memory/persistent.py +515 -0
- bareagent/memory/token_counter.py +67 -0
- bareagent/memory/token_tracker.py +262 -0
- bareagent/memory/transcript.py +100 -0
- bareagent/permission/__init__.py +1 -0
- bareagent/permission/guard.py +329 -0
- bareagent/permission/rules.py +19 -0
- bareagent/planning/__init__.py +19 -0
- bareagent/planning/agent_types.py +169 -0
- bareagent/planning/skill_gen.py +141 -0
- bareagent/planning/skill_store.py +173 -0
- bareagent/planning/skills.py +146 -0
- bareagent/planning/subagent.py +355 -0
- bareagent/planning/subagent_registry.py +77 -0
- bareagent/planning/tasks.py +348 -0
- bareagent/planning/todo.py +153 -0
- bareagent/planning/worktree.py +122 -0
- bareagent/provider/__init__.py +1 -0
- bareagent/provider/anthropic.py +348 -0
- bareagent/provider/base.py +136 -0
- bareagent/provider/factory.py +130 -0
- bareagent/provider/openai.py +881 -0
- bareagent/provider/presets.py +72 -0
- bareagent/provider/setup.py +356 -0
- bareagent/skills/.gitkeep +1 -0
- bareagent/skills/code-review/SKILL.md +68 -0
- bareagent/skills/git/SKILL.md +68 -0
- bareagent/skills/test/SKILL.md +70 -0
- bareagent/team/__init__.py +17 -0
- bareagent/team/autonomous.py +193 -0
- bareagent/team/mailbox.py +239 -0
- bareagent/team/manager.py +155 -0
- bareagent/team/protocols.py +129 -0
- bareagent/tracing/__init__.py +12 -0
- bareagent/tracing/_api.py +92 -0
- bareagent/tracing/_proxy.py +60 -0
- bareagent/tracing/composite.py +115 -0
- bareagent/tracing/json_file.py +115 -0
- bareagent/tracing/langfuse.py +139 -0
- bareagent/tracing/otel.py +107 -0
- bareagent/tracing/setup.py +85 -0
- bareagent/ui/__init__.py +24 -0
- bareagent/ui/console.py +167 -0
- bareagent/ui/prompt.py +78 -0
- bareagent/ui/protocol.py +24 -0
- bareagent/ui/stream.py +66 -0
- bareagent/ui/theme.py +240 -0
- bareagent_cli-0.1.0.dist-info/METADATA +331 -0
- bareagent_cli-0.1.0.dist-info/RECORD +121 -0
- bareagent_cli-0.1.0.dist-info/WHEEL +4 -0
- bareagent_cli-0.1.0.dist-info/entry_points.txt +2 -0
- bareagent_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from functools import partial
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from bareagent.concurrency.background import BackgroundManager
|
|
9
|
+
from bareagent.core.fileutil import generate_random_id
|
|
10
|
+
from bareagent.core.loop import agent_loop
|
|
11
|
+
from bareagent.memory.compact import Compactor
|
|
12
|
+
from bareagent.permission.guard import PermissionGuard
|
|
13
|
+
from bareagent.planning.agent_types import (
|
|
14
|
+
BUILTIN_AGENT_TYPES,
|
|
15
|
+
DEFAULT_AGENT_TYPE,
|
|
16
|
+
AgentType,
|
|
17
|
+
filter_handlers,
|
|
18
|
+
filter_tools,
|
|
19
|
+
resolve_agent_type,
|
|
20
|
+
)
|
|
21
|
+
from bareagent.planning.subagent_registry import ResumableContext, SubagentRegistry
|
|
22
|
+
from bareagent.planning.worktree import (
|
|
23
|
+
WorktreeError,
|
|
24
|
+
WorktreeHandle,
|
|
25
|
+
create_worktree,
|
|
26
|
+
is_git_repo,
|
|
27
|
+
remove_worktree,
|
|
28
|
+
worktree_status,
|
|
29
|
+
)
|
|
30
|
+
from bareagent.provider.base import BaseLLMProvider
|
|
31
|
+
from bareagent.tracing import tracer as global_tracer
|
|
32
|
+
|
|
33
|
+
_SUBAGENT_COMPACT_THRESHOLD = 50_000
|
|
34
|
+
|
|
35
|
+
# Memory commands that mutate the store. Read-only agent types may only
|
|
36
|
+
# ``view``; the rest are rejected by ``_make_readonly_memory_handler``.
|
|
37
|
+
_MEMORY_WRITE_COMMANDS = frozenset({"create", "str_replace", "insert", "delete", "rename"})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _make_readonly_memory_handler(inner: Any) -> Any:
|
|
41
|
+
"""Wrap a ``memory`` handler so write commands are refused.
|
|
42
|
+
|
|
43
|
+
The ``memory`` tool is a single tool with a ``command`` enum, so it cannot
|
|
44
|
+
be removed by name-filtering the way ``mcp__*`` / ``lsp_*`` tools are.
|
|
45
|
+
Instead we downgrade it here, at the boundary where the child agent type is
|
|
46
|
+
known, leaving ``view`` available for recall.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def _wrapped(**kwargs: Any) -> Any:
|
|
50
|
+
command = str(kwargs.get("command", "")).strip()
|
|
51
|
+
if command in _MEMORY_WRITE_COMMANDS:
|
|
52
|
+
return (
|
|
53
|
+
"Error: memory is read-only for this agent type; only the "
|
|
54
|
+
"'view' command is permitted."
|
|
55
|
+
)
|
|
56
|
+
return inner(**kwargs)
|
|
57
|
+
|
|
58
|
+
return _wrapped
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _build_subagent_description() -> str:
|
|
62
|
+
lines = [
|
|
63
|
+
"Delegate a self-contained task to a child agent with isolated messages.",
|
|
64
|
+
"Available agent types:",
|
|
65
|
+
]
|
|
66
|
+
for name, at in BUILTIN_AGENT_TYPES.items():
|
|
67
|
+
lines.append(f"- {name}: {at.description}")
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
SUBAGENT_TOOL_SCHEMAS: list[dict[str, Any]] = [
|
|
72
|
+
{
|
|
73
|
+
"name": "subagent",
|
|
74
|
+
"description": _build_subagent_description(),
|
|
75
|
+
"parameters": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"properties": {
|
|
78
|
+
"task": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "The task for the child agent to complete.",
|
|
81
|
+
},
|
|
82
|
+
"agent_type": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "Optional child-agent profile to use.",
|
|
85
|
+
"enum": list(BUILTIN_AGENT_TYPES.keys()),
|
|
86
|
+
},
|
|
87
|
+
"run_in_background": {
|
|
88
|
+
"type": "boolean",
|
|
89
|
+
"description": "Run the child agent asynchronously in the background.",
|
|
90
|
+
"default": False,
|
|
91
|
+
},
|
|
92
|
+
"isolation": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"enum": ["none", "worktree"],
|
|
95
|
+
"default": "none",
|
|
96
|
+
"description": (
|
|
97
|
+
"Set 'worktree' to run the child agent in an isolated git "
|
|
98
|
+
"worktree + temp branch; file ops won't touch the main working tree."
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
"required": ["task"],
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def run_subagent(
|
|
109
|
+
provider: BaseLLMProvider,
|
|
110
|
+
task: str,
|
|
111
|
+
tools: list[dict[str, Any]],
|
|
112
|
+
handlers: dict[str, Any],
|
|
113
|
+
permission: Any,
|
|
114
|
+
system_prompt: str = "",
|
|
115
|
+
max_depth: int = 3,
|
|
116
|
+
current_depth: int = 1,
|
|
117
|
+
agent_type: str | None = None,
|
|
118
|
+
bg_manager: BackgroundManager | None = None,
|
|
119
|
+
run_in_background: bool = False,
|
|
120
|
+
default_agent_type: str = DEFAULT_AGENT_TYPE,
|
|
121
|
+
isolation: str = "none",
|
|
122
|
+
retry_policy: Any = None,
|
|
123
|
+
registry: SubagentRegistry | None = None,
|
|
124
|
+
token_tracker: Any = None,
|
|
125
|
+
) -> str:
|
|
126
|
+
if current_depth > max_depth:
|
|
127
|
+
return f"Subagent refused: recursion depth {current_depth} exceeds limit {max_depth}."
|
|
128
|
+
|
|
129
|
+
resolved_type = resolve_agent_type(agent_type, default_name=default_agent_type)
|
|
130
|
+
child_permission = _build_child_permission(
|
|
131
|
+
permission=permission,
|
|
132
|
+
agent_type=resolved_type,
|
|
133
|
+
background=run_in_background,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if run_in_background:
|
|
137
|
+
if bg_manager is None:
|
|
138
|
+
return (
|
|
139
|
+
"Subagent background execution unavailable: background manager is not configured."
|
|
140
|
+
)
|
|
141
|
+
task_id = _generate_subagent_task_id()
|
|
142
|
+
bg_manager.submit(
|
|
143
|
+
task_id,
|
|
144
|
+
partial(
|
|
145
|
+
_run_subagent_sync,
|
|
146
|
+
provider=provider,
|
|
147
|
+
task=task,
|
|
148
|
+
tools=tools,
|
|
149
|
+
handlers=handlers,
|
|
150
|
+
permission=child_permission,
|
|
151
|
+
system_prompt=system_prompt,
|
|
152
|
+
max_depth=max_depth,
|
|
153
|
+
current_depth=current_depth,
|
|
154
|
+
resolved_type=resolved_type,
|
|
155
|
+
bg_manager=bg_manager,
|
|
156
|
+
default_agent_type=default_agent_type,
|
|
157
|
+
isolation=isolation,
|
|
158
|
+
retry_policy=retry_policy,
|
|
159
|
+
# Background subagents are fire-and-notify: their context is gone
|
|
160
|
+
# by the time a notification surfaces, so they are never resumable.
|
|
161
|
+
registry=None,
|
|
162
|
+
token_tracker=token_tracker,
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
return f"Subagent {task_id} started in the background."
|
|
166
|
+
|
|
167
|
+
return _run_subagent_sync(
|
|
168
|
+
provider=provider,
|
|
169
|
+
task=task,
|
|
170
|
+
tools=tools,
|
|
171
|
+
handlers=handlers,
|
|
172
|
+
permission=child_permission,
|
|
173
|
+
system_prompt=system_prompt,
|
|
174
|
+
max_depth=max_depth,
|
|
175
|
+
current_depth=current_depth,
|
|
176
|
+
resolved_type=resolved_type,
|
|
177
|
+
bg_manager=bg_manager,
|
|
178
|
+
default_agent_type=default_agent_type,
|
|
179
|
+
isolation=isolation,
|
|
180
|
+
retry_policy=retry_policy,
|
|
181
|
+
registry=registry,
|
|
182
|
+
token_tracker=token_tracker,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _run_subagent_sync(
|
|
187
|
+
provider: BaseLLMProvider,
|
|
188
|
+
task: str,
|
|
189
|
+
tools: list[dict[str, Any]],
|
|
190
|
+
handlers: dict[str, Any],
|
|
191
|
+
permission: Any,
|
|
192
|
+
system_prompt: str,
|
|
193
|
+
max_depth: int,
|
|
194
|
+
current_depth: int,
|
|
195
|
+
resolved_type: AgentType,
|
|
196
|
+
bg_manager: BackgroundManager | None,
|
|
197
|
+
default_agent_type: str,
|
|
198
|
+
isolation: str = "none",
|
|
199
|
+
retry_policy: Any = None,
|
|
200
|
+
registry: SubagentRegistry | None = None,
|
|
201
|
+
token_tracker: Any = None,
|
|
202
|
+
) -> str:
|
|
203
|
+
filtered_tools = filter_tools(tools, resolved_type)
|
|
204
|
+
child_handlers = filter_handlers(handlers, filtered_tools)
|
|
205
|
+
if not resolved_type.memory_writable and "memory" in child_handlers:
|
|
206
|
+
child_handlers["memory"] = _make_readonly_memory_handler(child_handlers["memory"])
|
|
207
|
+
resolved_system_prompt = _compose_system_prompt(
|
|
208
|
+
parent_prompt=system_prompt,
|
|
209
|
+
agent_prompt=resolved_type.system_prompt,
|
|
210
|
+
)
|
|
211
|
+
compact_fn = Compactor(
|
|
212
|
+
provider=provider,
|
|
213
|
+
transcript_mgr=None,
|
|
214
|
+
threshold=_SUBAGENT_COMPACT_THRESHOLD,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Worktree isolation: rebind the six file-op handlers onto the worktree
|
|
218
|
+
# path *before* the nested subagent closure is built, so a child spawned
|
|
219
|
+
# inside this worktree also writes into the worktree. fail-open: a non-git
|
|
220
|
+
# workspace or a failed worktree creation falls back to no isolation with a
|
|
221
|
+
# footnote (isolation is a convenience, PermissionGuard is the safety edge).
|
|
222
|
+
handle: WorktreeHandle | None = None
|
|
223
|
+
footnote = ""
|
|
224
|
+
if isolation == "worktree":
|
|
225
|
+
base = os.getcwd()
|
|
226
|
+
if not is_git_repo(base):
|
|
227
|
+
footnote = "\n\n[worktree] skipped: not a git repository (ran without isolation)."
|
|
228
|
+
else:
|
|
229
|
+
try:
|
|
230
|
+
handle = create_worktree(base)
|
|
231
|
+
except WorktreeError as exc:
|
|
232
|
+
footnote = f"\n\n[worktree] skipped: {exc} (ran without isolation)."
|
|
233
|
+
else:
|
|
234
|
+
# Lazy import: ``bareagent.core.tools`` imports this module at top
|
|
235
|
+
# level, so importing it here breaks the cycle.
|
|
236
|
+
from bareagent.core.tools import rebind_workspace_handlers
|
|
237
|
+
|
|
238
|
+
child_handlers = rebind_workspace_handlers(child_handlers, Path(handle.path))
|
|
239
|
+
|
|
240
|
+
if "subagent" in child_handlers:
|
|
241
|
+
# Capture the (possibly rebound) child_handlers so a nested subagent
|
|
242
|
+
# inherits the worktree-rooted file handlers. Nested isolation defaults
|
|
243
|
+
# to "none" (no worktree-in-worktree, per Out of Scope).
|
|
244
|
+
nested_handlers = child_handlers
|
|
245
|
+
child_handlers["subagent"] = (
|
|
246
|
+
lambda task, agent_type=None, run_in_background=False, isolation="none": run_subagent(
|
|
247
|
+
provider=provider,
|
|
248
|
+
task=task,
|
|
249
|
+
tools=filtered_tools,
|
|
250
|
+
handlers=nested_handlers,
|
|
251
|
+
permission=permission,
|
|
252
|
+
system_prompt=resolved_system_prompt,
|
|
253
|
+
max_depth=max_depth,
|
|
254
|
+
current_depth=current_depth + 1,
|
|
255
|
+
agent_type=agent_type,
|
|
256
|
+
bg_manager=bg_manager,
|
|
257
|
+
run_in_background=run_in_background,
|
|
258
|
+
default_agent_type=default_agent_type,
|
|
259
|
+
isolation=isolation,
|
|
260
|
+
retry_policy=retry_policy,
|
|
261
|
+
# Nested subagents are not directly resumable from the main loop
|
|
262
|
+
# ("you can only continue agents you spawned"): never register.
|
|
263
|
+
registry=None,
|
|
264
|
+
token_tracker=token_tracker,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
messages: list[dict[str, Any]] = []
|
|
269
|
+
if resolved_system_prompt.strip():
|
|
270
|
+
messages.append({"role": "system", "content": resolved_system_prompt})
|
|
271
|
+
messages.append({"role": "user", "content": task})
|
|
272
|
+
try:
|
|
273
|
+
with global_tracer.trace(
|
|
274
|
+
"subagent",
|
|
275
|
+
tags={"agent_type": resolved_type.name, "depth": current_depth},
|
|
276
|
+
) as span:
|
|
277
|
+
span.set_content_tag("task", task)
|
|
278
|
+
result = agent_loop(
|
|
279
|
+
provider=provider,
|
|
280
|
+
messages=messages,
|
|
281
|
+
tools=filtered_tools,
|
|
282
|
+
handlers=child_handlers,
|
|
283
|
+
permission=permission,
|
|
284
|
+
compact_fn=compact_fn,
|
|
285
|
+
bg_manager=None,
|
|
286
|
+
max_iterations=resolved_type.max_turns,
|
|
287
|
+
retry_policy=retry_policy,
|
|
288
|
+
token_tracker=token_tracker,
|
|
289
|
+
)
|
|
290
|
+
span.set_content_tag("result", result[:500])
|
|
291
|
+
finally:
|
|
292
|
+
if handle is not None:
|
|
293
|
+
footnote = _finalize_worktree(handle)
|
|
294
|
+
|
|
295
|
+
# Register a resumable context only for a foreground, non-worktree subagent
|
|
296
|
+
# that completed successfully (reaching here means agent_loop did not raise).
|
|
297
|
+
# ``messages`` is the live list agent_loop mutated, so resuming re-enters the
|
|
298
|
+
# same conversation. Worktree subagents are excluded: their isolated tree is
|
|
299
|
+
# finalized above, so a later resume would write into the wrong place.
|
|
300
|
+
if registry is not None and isolation == "none":
|
|
301
|
+
agent_id = registry.generate_id()
|
|
302
|
+
registry.register(
|
|
303
|
+
ResumableContext(
|
|
304
|
+
agent_id=agent_id,
|
|
305
|
+
messages=messages,
|
|
306
|
+
provider=provider,
|
|
307
|
+
tools=filtered_tools,
|
|
308
|
+
handlers=child_handlers,
|
|
309
|
+
permission=permission,
|
|
310
|
+
compact_fn=compact_fn,
|
|
311
|
+
max_turns=resolved_type.max_turns,
|
|
312
|
+
retry_policy=retry_policy,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
footnote += (
|
|
316
|
+
f"\n\n[subagent id {agent_id}: resumable -- "
|
|
317
|
+
"continue this conversation with subagent_send]"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return result + footnote
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _finalize_worktree(handle: WorktreeHandle) -> str:
|
|
324
|
+
"""Keep a dirty worktree (with a report) or clean up a pristine one.
|
|
325
|
+
|
|
326
|
+
Returns the footnote to append to the sub-agent's result.
|
|
327
|
+
"""
|
|
328
|
+
dirty, summary = worktree_status(handle.path)
|
|
329
|
+
if dirty:
|
|
330
|
+
return (
|
|
331
|
+
f"\n\n[worktree] kept at {handle.path} on branch {handle.branch} "
|
|
332
|
+
f"({summary}). Inspect with: git worktree list."
|
|
333
|
+
)
|
|
334
|
+
remove_worktree(handle)
|
|
335
|
+
return f"\n\n[worktree] cleaned up (no changes) at branch {handle.branch}."
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _compose_system_prompt(*, parent_prompt: str, agent_prompt: str) -> str:
|
|
339
|
+
parts = [p for raw in (parent_prompt, agent_prompt) if (p := raw.strip())]
|
|
340
|
+
return "\n\n".join(parts)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _build_child_permission(
|
|
344
|
+
*,
|
|
345
|
+
permission: Any,
|
|
346
|
+
agent_type: AgentType,
|
|
347
|
+
background: bool,
|
|
348
|
+
) -> Any:
|
|
349
|
+
if isinstance(permission, PermissionGuard):
|
|
350
|
+
return permission.for_subagent(agent_type, background=background)
|
|
351
|
+
return permission
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _generate_subagent_task_id() -> str:
|
|
355
|
+
return "subagent-" + generate_random_id(8)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from bareagent.core.fileutil import generate_random_id
|
|
7
|
+
|
|
8
|
+
_ID_PREFIX = "sa-"
|
|
9
|
+
DEFAULT_MAX_RESUMABLE = 20
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class ResumableContext:
|
|
14
|
+
"""A foreground subagent's runtime, captured so it can re-enter ``agent_loop``.
|
|
15
|
+
|
|
16
|
+
``messages`` is the live conversation list that ``agent_loop`` appends to in
|
|
17
|
+
place, so a resumed context stays current without being re-stored. The
|
|
18
|
+
remaining fields are exactly the bindings ``agent_loop`` needs to resume the
|
|
19
|
+
same isolated child (provider / tools / handlers / permission / compactor /
|
|
20
|
+
turn budget / retry policy).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
agent_id: str
|
|
24
|
+
messages: list[dict[str, Any]]
|
|
25
|
+
provider: Any
|
|
26
|
+
tools: list[dict[str, Any]]
|
|
27
|
+
handlers: dict[str, Any]
|
|
28
|
+
permission: Any
|
|
29
|
+
compact_fn: Any
|
|
30
|
+
max_turns: int
|
|
31
|
+
retry_policy: Any = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SubagentRegistry:
|
|
35
|
+
"""Session-scoped, in-memory store of resumable foreground subagents.
|
|
36
|
+
|
|
37
|
+
Insertion-ordered: ``register`` moves an existing id to the end (most-recently
|
|
38
|
+
touched) so the FIFO eviction of the oldest entry never drops a context that
|
|
39
|
+
is part of an active multi-turn conversation. Holds at most ``max_resumable``
|
|
40
|
+
contexts; registering past the cap evicts the oldest. This mirrors the
|
|
41
|
+
session-scoped lifecycle of ``spawned_agents`` -- the REPL calls ``clear`` on
|
|
42
|
+
``/new`` / ``/resume`` / ``/import`` / ``/clear`` and leaves it intact across
|
|
43
|
+
``/compact``.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, max_resumable: int = DEFAULT_MAX_RESUMABLE) -> None:
|
|
47
|
+
self._max = max_resumable if max_resumable > 0 else DEFAULT_MAX_RESUMABLE
|
|
48
|
+
self._contexts: dict[str, ResumableContext] = {}
|
|
49
|
+
|
|
50
|
+
def generate_id(self) -> str:
|
|
51
|
+
"""Return a fresh, unused ``sa-<rand8>`` id."""
|
|
52
|
+
while True:
|
|
53
|
+
candidate = _ID_PREFIX + generate_random_id(8)
|
|
54
|
+
if candidate not in self._contexts:
|
|
55
|
+
return candidate
|
|
56
|
+
|
|
57
|
+
def register(self, context: ResumableContext) -> None:
|
|
58
|
+
"""Store *context*, refreshing its position and evicting the oldest if over cap."""
|
|
59
|
+
# pop + re-insert => most-recently touched moves to the end, so the
|
|
60
|
+
# oldest-by-touch entry is the one evicted when we exceed the cap.
|
|
61
|
+
self._contexts.pop(context.agent_id, None)
|
|
62
|
+
self._contexts[context.agent_id] = context
|
|
63
|
+
while len(self._contexts) > self._max:
|
|
64
|
+
oldest = next(iter(self._contexts))
|
|
65
|
+
del self._contexts[oldest]
|
|
66
|
+
|
|
67
|
+
def get(self, agent_id: str) -> ResumableContext | None:
|
|
68
|
+
return self._contexts.get(agent_id)
|
|
69
|
+
|
|
70
|
+
def has(self, agent_id: str) -> bool:
|
|
71
|
+
return agent_id in self._contexts
|
|
72
|
+
|
|
73
|
+
def clear(self) -> None:
|
|
74
|
+
self._contexts.clear()
|
|
75
|
+
|
|
76
|
+
def __len__(self) -> int:
|
|
77
|
+
return len(self._contexts)
|