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.
Files changed (121) hide show
  1. bareagent/__init__.py +10 -0
  2. bareagent/concurrency/__init__.py +6 -0
  3. bareagent/concurrency/background.py +97 -0
  4. bareagent/concurrency/notification.py +61 -0
  5. bareagent/concurrency/scheduler.py +136 -0
  6. bareagent/config.toml +299 -0
  7. bareagent/core/__init__.py +1 -0
  8. bareagent/core/config_paths.py +49 -0
  9. bareagent/core/context.py +127 -0
  10. bareagent/core/fileutil.py +103 -0
  11. bareagent/core/goal.py +214 -0
  12. bareagent/core/handlers/__init__.py +1 -0
  13. bareagent/core/handlers/bash.py +79 -0
  14. bareagent/core/handlers/file_edit.py +47 -0
  15. bareagent/core/handlers/file_read.py +270 -0
  16. bareagent/core/handlers/file_write.py +34 -0
  17. bareagent/core/handlers/glob_search.py +30 -0
  18. bareagent/core/handlers/goal.py +60 -0
  19. bareagent/core/handlers/grep_search.py +52 -0
  20. bareagent/core/handlers/memory.py +71 -0
  21. bareagent/core/handlers/plan.py +106 -0
  22. bareagent/core/handlers/search_utils.py +77 -0
  23. bareagent/core/handlers/skill.py +87 -0
  24. bareagent/core/handlers/subagent_send.py +70 -0
  25. bareagent/core/handlers/web_fetch.py +126 -0
  26. bareagent/core/handlers/web_search.py +165 -0
  27. bareagent/core/handlers/workflow.py +190 -0
  28. bareagent/core/loop.py +535 -0
  29. bareagent/core/retry.py +131 -0
  30. bareagent/core/sandbox.py +27 -0
  31. bareagent/core/schema.py +21 -0
  32. bareagent/core/tools.py +779 -0
  33. bareagent/core/workflow.py +517 -0
  34. bareagent/core/workflow_registry.py +219 -0
  35. bareagent/debug/__init__.py +0 -0
  36. bareagent/debug/interaction_log.py +263 -0
  37. bareagent/debug/viewer.html +1750 -0
  38. bareagent/debug/web_viewer.py +157 -0
  39. bareagent/hooks/__init__.py +32 -0
  40. bareagent/hooks/config.py +118 -0
  41. bareagent/hooks/engine.py +197 -0
  42. bareagent/hooks/errors.py +14 -0
  43. bareagent/hooks/events.py +22 -0
  44. bareagent/lsp/__init__.py +63 -0
  45. bareagent/lsp/config.py +134 -0
  46. bareagent/lsp/coord.py +118 -0
  47. bareagent/lsp/diagnostics.py +240 -0
  48. bareagent/lsp/errors.py +24 -0
  49. bareagent/lsp/manager.py +866 -0
  50. bareagent/lsp/tools.py +629 -0
  51. bareagent/lsp/workspace_edit.py +305 -0
  52. bareagent/main.py +4205 -0
  53. bareagent/mcp/__init__.py +69 -0
  54. bareagent/mcp/_sse.py +69 -0
  55. bareagent/mcp/client.py +341 -0
  56. bareagent/mcp/config.py +169 -0
  57. bareagent/mcp/errors.py +32 -0
  58. bareagent/mcp/manager.py +318 -0
  59. bareagent/mcp/protocol.py +187 -0
  60. bareagent/mcp/registry.py +557 -0
  61. bareagent/mcp/transport/__init__.py +15 -0
  62. bareagent/mcp/transport/base.py +149 -0
  63. bareagent/mcp/transport/http_legacy.py +192 -0
  64. bareagent/mcp/transport/http_streamable.py +217 -0
  65. bareagent/mcp/transport/stdio.py +202 -0
  66. bareagent/memory/__init__.py +1 -0
  67. bareagent/memory/compact.py +203 -0
  68. bareagent/memory/conversation_io.py +226 -0
  69. bareagent/memory/embedding.py +194 -0
  70. bareagent/memory/persistent.py +515 -0
  71. bareagent/memory/token_counter.py +67 -0
  72. bareagent/memory/token_tracker.py +262 -0
  73. bareagent/memory/transcript.py +100 -0
  74. bareagent/permission/__init__.py +1 -0
  75. bareagent/permission/guard.py +329 -0
  76. bareagent/permission/rules.py +19 -0
  77. bareagent/planning/__init__.py +19 -0
  78. bareagent/planning/agent_types.py +169 -0
  79. bareagent/planning/skill_gen.py +141 -0
  80. bareagent/planning/skill_store.py +173 -0
  81. bareagent/planning/skills.py +146 -0
  82. bareagent/planning/subagent.py +355 -0
  83. bareagent/planning/subagent_registry.py +77 -0
  84. bareagent/planning/tasks.py +348 -0
  85. bareagent/planning/todo.py +153 -0
  86. bareagent/planning/worktree.py +122 -0
  87. bareagent/provider/__init__.py +1 -0
  88. bareagent/provider/anthropic.py +348 -0
  89. bareagent/provider/base.py +136 -0
  90. bareagent/provider/factory.py +130 -0
  91. bareagent/provider/openai.py +881 -0
  92. bareagent/provider/presets.py +72 -0
  93. bareagent/provider/setup.py +356 -0
  94. bareagent/skills/.gitkeep +1 -0
  95. bareagent/skills/code-review/SKILL.md +68 -0
  96. bareagent/skills/git/SKILL.md +68 -0
  97. bareagent/skills/test/SKILL.md +70 -0
  98. bareagent/team/__init__.py +17 -0
  99. bareagent/team/autonomous.py +193 -0
  100. bareagent/team/mailbox.py +239 -0
  101. bareagent/team/manager.py +155 -0
  102. bareagent/team/protocols.py +129 -0
  103. bareagent/tracing/__init__.py +12 -0
  104. bareagent/tracing/_api.py +92 -0
  105. bareagent/tracing/_proxy.py +60 -0
  106. bareagent/tracing/composite.py +115 -0
  107. bareagent/tracing/json_file.py +115 -0
  108. bareagent/tracing/langfuse.py +139 -0
  109. bareagent/tracing/otel.py +107 -0
  110. bareagent/tracing/setup.py +85 -0
  111. bareagent/ui/__init__.py +24 -0
  112. bareagent/ui/console.py +167 -0
  113. bareagent/ui/prompt.py +78 -0
  114. bareagent/ui/protocol.py +24 -0
  115. bareagent/ui/stream.py +66 -0
  116. bareagent/ui/theme.py +240 -0
  117. bareagent_cli-0.1.0.dist-info/METADATA +331 -0
  118. bareagent_cli-0.1.0.dist-info/RECORD +121 -0
  119. bareagent_cli-0.1.0.dist-info/WHEEL +4 -0
  120. bareagent_cli-0.1.0.dist-info/entry_points.txt +2 -0
  121. 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)