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
bareagent/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""BareAgent package."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("bareagent-cli")
|
|
7
|
+
except PackageNotFoundError: # running from a source tree that was never installed
|
|
8
|
+
__version__ = "0.0.0+unknown"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import queue
|
|
4
|
+
import threading
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BackgroundManager:
|
|
10
|
+
"""Run slow operations in daemon threads and collect completion notifications."""
|
|
11
|
+
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self._queue: queue.Queue[dict[str, Any]] = queue.Queue()
|
|
14
|
+
self._threads: dict[str, threading.Thread] = {}
|
|
15
|
+
self._lock = threading.Lock()
|
|
16
|
+
|
|
17
|
+
def notify(self, task_id: str, message: str, *, status: str = "failed") -> None:
|
|
18
|
+
"""Post an external notification onto the same channel as task completions.
|
|
19
|
+
|
|
20
|
+
Used by subsystems that have nothing to run in a background thread but
|
|
21
|
+
still want their event surfaced through the REPL's background-update
|
|
22
|
+
injection (see ``src/concurrency/notification.py``). MCP disconnect
|
|
23
|
+
events flow through here so the user sees them between LLM turns even
|
|
24
|
+
when no MCP tool was in-flight.
|
|
25
|
+
"""
|
|
26
|
+
self._queue.put(
|
|
27
|
+
{
|
|
28
|
+
"task_id": task_id,
|
|
29
|
+
"status": status,
|
|
30
|
+
"error": message,
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def submit(self, task_id: str, fn: Callable[..., Any], *args: Any) -> str:
|
|
35
|
+
with self._lock:
|
|
36
|
+
# Prune dead threads to prevent unbounded growth.
|
|
37
|
+
self._threads = {tid: t for tid, t in self._threads.items() if t.is_alive()}
|
|
38
|
+
active_thread = self._threads.get(task_id)
|
|
39
|
+
if active_thread is not None:
|
|
40
|
+
raise ValueError(f"Background task already running: {task_id}")
|
|
41
|
+
|
|
42
|
+
thread = threading.Thread(
|
|
43
|
+
target=self._run,
|
|
44
|
+
args=(task_id, fn, *args),
|
|
45
|
+
daemon=True,
|
|
46
|
+
)
|
|
47
|
+
self._threads[task_id] = thread
|
|
48
|
+
thread.start()
|
|
49
|
+
return task_id
|
|
50
|
+
|
|
51
|
+
def is_running(self, task_id: str) -> bool:
|
|
52
|
+
"""Return True if a live (not-yet-finished) thread is registered for ``task_id``.
|
|
53
|
+
|
|
54
|
+
Read-only liveness probe used by callers that track long-lived background
|
|
55
|
+
work by id (e.g. team teammates registered as ``team:<session>:<name>``).
|
|
56
|
+
Mirrors the ``is_alive()`` checks already used by ``submit`` /
|
|
57
|
+
``drain_notifications`` without mutating the thread registry.
|
|
58
|
+
"""
|
|
59
|
+
with self._lock:
|
|
60
|
+
thread = self._threads.get(task_id)
|
|
61
|
+
return thread is not None and thread.is_alive()
|
|
62
|
+
|
|
63
|
+
def _run(self, task_id: str, fn: Callable[..., Any], *args: Any) -> None:
|
|
64
|
+
try:
|
|
65
|
+
result = fn(*args)
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
self._queue.put(
|
|
68
|
+
{
|
|
69
|
+
"task_id": task_id,
|
|
70
|
+
"status": "failed",
|
|
71
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
self._queue.put(
|
|
77
|
+
{
|
|
78
|
+
"task_id": task_id,
|
|
79
|
+
"status": "done",
|
|
80
|
+
"result": result,
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def drain_notifications(self) -> list[dict[str, Any]]:
|
|
85
|
+
notifications: list[dict[str, Any]] = []
|
|
86
|
+
while True:
|
|
87
|
+
try:
|
|
88
|
+
notifications.append(self._queue.get_nowait())
|
|
89
|
+
except queue.Empty:
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
with self._lock:
|
|
93
|
+
dead = [tid for tid, t in self._threads.items() if not t.is_alive()]
|
|
94
|
+
for tid in dead:
|
|
95
|
+
del self._threads[tid]
|
|
96
|
+
|
|
97
|
+
return notifications
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from bareagent.concurrency.background import BackgroundManager
|
|
6
|
+
from bareagent.core.fileutil import is_tool_result_message, stringify
|
|
7
|
+
|
|
8
|
+
_NOTIFICATION_PREFIX = "<background-notifications>"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def inject_notifications(
|
|
12
|
+
messages: list[dict[str, Any]],
|
|
13
|
+
bg_manager: BackgroundManager,
|
|
14
|
+
) -> None:
|
|
15
|
+
notifications = bg_manager.drain_notifications()
|
|
16
|
+
if not notifications:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
lines = ["后台任务更新:"]
|
|
20
|
+
surfaced = 0
|
|
21
|
+
for notification in notifications:
|
|
22
|
+
task_id = str(notification.get("task_id", "unknown"))
|
|
23
|
+
# Workflow runs (task_id ``wf-<id>``) are delivered in full by the
|
|
24
|
+
# dedicated _drain_workflow_results channel; skip them here so their
|
|
25
|
+
# summary is not also injected truncated through this generic path.
|
|
26
|
+
if task_id.startswith("wf-"):
|
|
27
|
+
continue
|
|
28
|
+
surfaced += 1
|
|
29
|
+
status = str(notification.get("status", "unknown"))
|
|
30
|
+
detail = ""
|
|
31
|
+
if "result" in notification:
|
|
32
|
+
result_text = stringify(notification["result"])
|
|
33
|
+
if result_text:
|
|
34
|
+
detail = f" - {result_text[:500]}"
|
|
35
|
+
elif "error" in notification:
|
|
36
|
+
error_text = stringify(notification["error"])
|
|
37
|
+
if error_text:
|
|
38
|
+
detail = f" - {error_text[:500]}"
|
|
39
|
+
lines.append(f"- Task {task_id}: {status}{detail}")
|
|
40
|
+
|
|
41
|
+
# Every notification was a workflow run (delivered elsewhere) -> nothing to
|
|
42
|
+
# inject here.
|
|
43
|
+
if surfaced == 0:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
notification_message = {
|
|
47
|
+
"role": "system",
|
|
48
|
+
"content": (
|
|
49
|
+
f"{_NOTIFICATION_PREFIX}\n"
|
|
50
|
+
+ "\n".join(lines)
|
|
51
|
+
+ "\n</background-notifications>"
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
if messages and messages[-1].get("role") == "user":
|
|
55
|
+
if is_tool_result_message(messages[-1]):
|
|
56
|
+
messages.append(notification_message)
|
|
57
|
+
else:
|
|
58
|
+
messages.insert(len(messages) - 1, notification_message)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
messages.append(notification_message)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from bareagent.core.fileutil import generate_random_id
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Guard against `/loop 0 ...` hammering the background pool. Intervals below
|
|
14
|
+
# this floor are rejected at `add()` time.
|
|
15
|
+
MIN_INTERVAL_SEC = 5.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SchedulerError(Exception):
|
|
19
|
+
"""Raised when a scheduled job cannot be created (bad interval / command)."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class ScheduledJob:
|
|
24
|
+
"""A shell command repeated on a fixed interval until cancelled.
|
|
25
|
+
|
|
26
|
+
``run_count`` is mutated in place by ``Scheduler._fire`` each time the job
|
|
27
|
+
triggers; it is the only field that changes after creation.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
job_id: str
|
|
31
|
+
interval_sec: float
|
|
32
|
+
command: str
|
|
33
|
+
run_count: int = field(default=0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Scheduler:
|
|
37
|
+
"""Fire shell commands on fixed intervals via repeated ``threading.Timer`` arms.
|
|
38
|
+
|
|
39
|
+
The scheduler only owns the *timing*: each fire hands the command to the
|
|
40
|
+
injected ``notifier`` (a ``BackgroundManager``) so execution and result
|
|
41
|
+
surfacing reuse the existing background-notification channel. The scheduler
|
|
42
|
+
never touches ``messages`` / ``console`` itself — that separation is what
|
|
43
|
+
keeps it thread-safe against the blocking REPL main loop.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
runner: Callable[[str], Any],
|
|
50
|
+
notifier: Any,
|
|
51
|
+
) -> None:
|
|
52
|
+
# ``runner`` receives a command string and runs it (REPL injects
|
|
53
|
+
# ``partial(run_bash, cwd=workspace, raise_on_error=True)``).
|
|
54
|
+
self._runner = runner
|
|
55
|
+
# ``notifier`` is the BackgroundManager; we only use ``submit``/``notify``.
|
|
56
|
+
self._notifier = notifier
|
|
57
|
+
self._lock = threading.Lock()
|
|
58
|
+
self._jobs: dict[str, ScheduledJob] = {}
|
|
59
|
+
self._timers: dict[str, threading.Timer] = {}
|
|
60
|
+
|
|
61
|
+
def add(self, interval_sec: float, command: str) -> ScheduledJob:
|
|
62
|
+
if interval_sec < MIN_INTERVAL_SEC:
|
|
63
|
+
raise SchedulerError(
|
|
64
|
+
f"Interval must be at least {MIN_INTERVAL_SEC:g} seconds (got {interval_sec:g})."
|
|
65
|
+
)
|
|
66
|
+
command = command.strip()
|
|
67
|
+
if not command:
|
|
68
|
+
raise SchedulerError("Command must not be empty.")
|
|
69
|
+
job_id = f"loop-{generate_random_id(6)}"
|
|
70
|
+
job = ScheduledJob(job_id=job_id, interval_sec=interval_sec, command=command)
|
|
71
|
+
with self._lock:
|
|
72
|
+
self._jobs[job_id] = job
|
|
73
|
+
self._arm(job)
|
|
74
|
+
return job
|
|
75
|
+
|
|
76
|
+
def _arm(self, job: ScheduledJob) -> None:
|
|
77
|
+
# Caller holds ``self._lock``. ``Timer.start`` only spins up a thread,
|
|
78
|
+
# so starting it under the lock is cheap and avoids a race where a
|
|
79
|
+
# concurrent ``cancel`` misses the freshly-armed timer.
|
|
80
|
+
timer = threading.Timer(job.interval_sec, self._fire, args=(job.job_id,))
|
|
81
|
+
timer.daemon = True
|
|
82
|
+
self._timers[job.job_id] = timer
|
|
83
|
+
timer.start()
|
|
84
|
+
|
|
85
|
+
def _fire(self, job_id: str) -> None:
|
|
86
|
+
# Runs on the Timer thread: nothing here may raise (a dying daemon
|
|
87
|
+
# thread is a silent debugging trap — see error-handling spec).
|
|
88
|
+
try:
|
|
89
|
+
with self._lock:
|
|
90
|
+
job = self._jobs.get(job_id)
|
|
91
|
+
if job is None:
|
|
92
|
+
# Cancelled between Timer fire and lock acquisition.
|
|
93
|
+
return
|
|
94
|
+
job.run_count += 1
|
|
95
|
+
run_id = f"{job_id}-{job.run_count}"
|
|
96
|
+
command = job.command
|
|
97
|
+
# Re-arm the next fire while still holding the lock so the
|
|
98
|
+
# repeat schedule is self-perpetuating until cancelled.
|
|
99
|
+
self._arm(job)
|
|
100
|
+
# Hand execution to the background pool outside the lock. A unique
|
|
101
|
+
# run_id per fire avoids BackgroundManager's same-task-id dedup
|
|
102
|
+
# ValueError; any submit failure is swallowed (surfaced via notify)
|
|
103
|
+
# so the schedule keeps running.
|
|
104
|
+
try:
|
|
105
|
+
self._notifier.submit(run_id, self._runner, command)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
try:
|
|
108
|
+
self._notifier.notify(
|
|
109
|
+
run_id,
|
|
110
|
+
f"Failed to dispatch scheduled command: {type(exc).__name__}: {exc}",
|
|
111
|
+
status="failed",
|
|
112
|
+
)
|
|
113
|
+
except Exception:
|
|
114
|
+
logger.exception("Scheduler notify failed for job %s", job_id)
|
|
115
|
+
except Exception:
|
|
116
|
+
logger.exception("Scheduler fire failed for job %s", job_id)
|
|
117
|
+
|
|
118
|
+
def list(self) -> list[ScheduledJob]:
|
|
119
|
+
with self._lock:
|
|
120
|
+
return list(self._jobs.values())
|
|
121
|
+
|
|
122
|
+
def cancel(self, job_id: str) -> bool:
|
|
123
|
+
with self._lock:
|
|
124
|
+
timer = self._timers.pop(job_id, None)
|
|
125
|
+
job = self._jobs.pop(job_id, None)
|
|
126
|
+
if timer is not None:
|
|
127
|
+
timer.cancel()
|
|
128
|
+
return job is not None
|
|
129
|
+
|
|
130
|
+
def cancel_all(self) -> None:
|
|
131
|
+
# Idempotent: safe to call multiple times (e.g. exit cleanup).
|
|
132
|
+
with self._lock:
|
|
133
|
+
for timer in self._timers.values():
|
|
134
|
+
timer.cancel()
|
|
135
|
+
self._timers.clear()
|
|
136
|
+
self._jobs.clear()
|
bareagent/config.toml
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Tip: run `bareagent init` for an interactive provider setup wizard that writes
|
|
2
|
+
# config.local.toml for you (DeepSeek / OpenAI / Anthropic / Qwen / GLM / custom).
|
|
3
|
+
#
|
|
4
|
+
# 配置热重载(ROADMAP 4.3):改完本文件 / config.local.toml 后,REPL 内 `/reload`
|
|
5
|
+
# 可热重载 `[ui] theme` 与 `[permission] mode/allow/deny`(改完即生效,无需重启);
|
|
6
|
+
# 其余配置(provider / mcp / lsp / hooks 等 boot 时固化的连接/客户端)仅报告
|
|
7
|
+
# “需重启”,重启 BareAgent 后才生效。改坏 TOML 时 `/reload` 报错并保持当前配置。
|
|
8
|
+
[provider]
|
|
9
|
+
name = "openai"
|
|
10
|
+
model = "gpt-4.1"
|
|
11
|
+
api_key_env = "OPENAI_API_KEY"
|
|
12
|
+
|
|
13
|
+
[permission]
|
|
14
|
+
mode = "default"
|
|
15
|
+
|
|
16
|
+
[ui]
|
|
17
|
+
stream = true
|
|
18
|
+
theme = "catppuccin-mocha"
|
|
19
|
+
|
|
20
|
+
[subagent]
|
|
21
|
+
max_depth = 3
|
|
22
|
+
default_type = "general-purpose"
|
|
23
|
+
# 可续跑前台子代理上下文的软上限(session 级、内存态)。subagent 跑完会注册一份
|
|
24
|
+
# 可续跑上下文,LLM 用返回的 agent id 经 subagent_send 续跑;超过此数按最旧淘汰。
|
|
25
|
+
# 仅前台、isolation="none" 的子代理可续跑(后台 / worktree 不注册)。restart-required。
|
|
26
|
+
max_resumable = 20
|
|
27
|
+
|
|
28
|
+
[thinking]
|
|
29
|
+
mode = "adaptive"
|
|
30
|
+
budget_tokens = 10000
|
|
31
|
+
|
|
32
|
+
[debug]
|
|
33
|
+
enabled = false
|
|
34
|
+
log_dir = ".logs"
|
|
35
|
+
viewer_port = 8321
|
|
36
|
+
pretty = true
|
|
37
|
+
|
|
38
|
+
[tracing]
|
|
39
|
+
# langfuse = false # 或设 LANGFUSE_PUBLIC_KEY 环境变量自动启用
|
|
40
|
+
# opentelemetry = false # 或设 OTEL_EXPORTER_OTLP_ENDPOINT 自动启用
|
|
41
|
+
# content_enabled = true # 是否上报消息内容(PII 敏感场景可关闭)
|
|
42
|
+
|
|
43
|
+
# --- MCP (Model Context Protocol) ----------------------------------------
|
|
44
|
+
# 在 [[mcp.servers]] 中声明外部 MCP server,BareAgent 启动时会并发拉起并把远端工具
|
|
45
|
+
# 按 `mcp__<name>__<tool>` 命名注入到工具列表;声明 resources capability 的 server
|
|
46
|
+
# 额外得到 `mcp__<name>__resource_list` / `mcp__<name>__resource_read`;prompts 可
|
|
47
|
+
# 通过 REPL slash 命令 `/mcp:<name>:<prompt>` 触发。REPL 命令:`/mcp status|list|reload <name>`。
|
|
48
|
+
[mcp]
|
|
49
|
+
# 单个 server initialize 握手超时(秒);超时后该 server 标 UNHEALTHY 并跳过,
|
|
50
|
+
# 不阻塞 REPL boot。
|
|
51
|
+
# start_timeout = 10
|
|
52
|
+
# 单次 tool result 文本字节上限(UTF-8),超出会被截断并追加
|
|
53
|
+
# `[truncated, original size: N bytes]` 后缀。256 KiB 对应主流 LLM context 友好范围。
|
|
54
|
+
# max_result_text_bytes = 262144
|
|
55
|
+
# 单个 image / embedded_resource binary 字节上限(base64 解码后估算),超出会被
|
|
56
|
+
# 替换为 `[Resource omitted: too large (N bytes)]` 占位文本。
|
|
57
|
+
# max_result_binary_bytes = 5242880
|
|
58
|
+
|
|
59
|
+
# 示例 1:stdio 子进程模式(mcp-server-fetch via uvx)。
|
|
60
|
+
# [[mcp.servers]]
|
|
61
|
+
# name = "fetch"
|
|
62
|
+
# transport = "stdio"
|
|
63
|
+
# command = "uvx"
|
|
64
|
+
# args = ["mcp-server-fetch"]
|
|
65
|
+
# # env = { FETCH_USER_AGENT = "BareAgent/0.1" }
|
|
66
|
+
# # cwd = "/path/to/workspace"
|
|
67
|
+
|
|
68
|
+
# 示例 2:HTTP Streamable (MCP 2025-03-26) 远端服务。
|
|
69
|
+
# [[mcp.servers]]
|
|
70
|
+
# name = "team-knowledge"
|
|
71
|
+
# transport = "http_streamable"
|
|
72
|
+
# url = "https://mcp.example.com/mcp"
|
|
73
|
+
# headers = { Authorization = "Bearer ${KNOWLEDGE_TOKEN}" }
|
|
74
|
+
# # transport 也可设为 "http_legacy"(MCP 2024-11-05 双端点)以兼容旧 server。
|
|
75
|
+
|
|
76
|
+
# --- LSP (Language Server Protocol) --------------------------------------
|
|
77
|
+
# 通过 multilspy 接入成熟 Language Server。MVP 暴露 4 个 Tier-1 工具:
|
|
78
|
+
# `lsp_outline` / `lsp_definition` / `lsp_references` / `lsp_diagnostics`。需安装可选 extra:
|
|
79
|
+
# uv pip install -e ".[lsp]"
|
|
80
|
+
# multilspy 0.0.15 内置语言适配:Python → jedi-language-server(非 pyright;jedi 走
|
|
81
|
+
# 符号导航强、类型诊断弱),TypeScript → typescript-language-server,Rust → rust-analyzer。
|
|
82
|
+
# REPL 命令:`/lsp status|list|reload <language>`。
|
|
83
|
+
[lsp]
|
|
84
|
+
# Hybrid auto-diagnostics-on-edit:edit_file / write_file 成功后自动调 LSP
|
|
85
|
+
# 拿最新诊断,与编辑前的快照按 (file, line, col, severity, message) 五元组
|
|
86
|
+
# 做 diff,将**新增**诊断追加到 tool result 末尾,格式:
|
|
87
|
+
# Newly introduced diagnostics in <file>:
|
|
88
|
+
# - [pyright Error] Line N:C — <message>
|
|
89
|
+
# 默认 OFF:保持 edit/write 体验与原版一致;启用前请确认 LSP server 已正常
|
|
90
|
+
# 起来(pyright 第一次全项目分析 5-15s)。
|
|
91
|
+
# auto_diagnostics_on_edit = false
|
|
92
|
+
# 单个 server handshake + initial analysis 超时(秒)。超时标 UNHEALTHY 并跳过,
|
|
93
|
+
# 不阻塞 REPL boot。pyright 通常 5-15s,rust-analyzer 项目较大时可能更久。
|
|
94
|
+
# start_timeout = 15.0
|
|
95
|
+
|
|
96
|
+
# 示例 1:Python(multilspy 默认走 jedi-language-server,不是 pyright)。
|
|
97
|
+
# [[lsp.servers]]
|
|
98
|
+
# language = "python" # multilspy code_language(→ JediServer)
|
|
99
|
+
# extensions = [".py", ".pyi"] # 文件扩展名 → 路由到此 server
|
|
100
|
+
# # initialization_options 透传给 LSP server(透传 LSP-specific 配置):
|
|
101
|
+
# # initialization_options = { python = { pythonPath = ".venv/bin/python" } }
|
|
102
|
+
|
|
103
|
+
# 示例 2:TypeScript / JavaScript(typescript-language-server)。
|
|
104
|
+
# [[lsp.servers]]
|
|
105
|
+
# language = "typescript"
|
|
106
|
+
# extensions = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"]
|
|
107
|
+
|
|
108
|
+
# 示例 3:Rust(rust-analyzer)。
|
|
109
|
+
# [[lsp.servers]]
|
|
110
|
+
# language = "rust"
|
|
111
|
+
# extensions = [".rs"]
|
|
112
|
+
|
|
113
|
+
# --- Persistent Memory ---------------------------------------------------
|
|
114
|
+
# 文件式跨会话记忆:一条记忆 = 一个带 frontmatter 的 .md 文件,MEMORY.md 作索引。
|
|
115
|
+
# 智能体通过单个 `memory` 工具(命令 view/create/str_replace/insert/delete/rename,
|
|
116
|
+
# 契约对齐 Anthropic memory tool,但注册为普通 client tool,全 provider 可用)读写受限的
|
|
117
|
+
# 记忆目录;会话开局把 MEMORY.md 索引 + MEMORY PROTOCOL 注入 system prompt。
|
|
118
|
+
# REPL 命令:`/remember <文本>`、`/forget <文本>`。子代理(explore/plan/code-review)只读。
|
|
119
|
+
[memory]
|
|
120
|
+
# 关闭后不注入记忆、不暴露可用的 memory 工具(调用返回 disabled 错误)。
|
|
121
|
+
enabled = true
|
|
122
|
+
# 记忆根目录。留空 = 每个项目独立目录 ~/.bareagent/projects/<workspace-slug>/memory/
|
|
123
|
+
# (不污染项目 git)。相对路径相对 workspace,绝对路径原样使用。
|
|
124
|
+
# dir = ""
|
|
125
|
+
# 会话开局注入 system prompt 的 MEMORY.md 索引最大行数。
|
|
126
|
+
# max_index_lines = 200
|
|
127
|
+
# 每轮按用户消息相关性召回并注入的记忆条数(0 = 关闭召回,仅保留开局索引注入)。
|
|
128
|
+
# recall_k = 5
|
|
129
|
+
# 语义/向量召回(task 06-08-semantic-memory-recall):默认 false = 词法召回(行为不变)。
|
|
130
|
+
# 开启后按 embedding 余弦相似度召回(换了措辞也能命中),embedding 不可用时自动回退词法。
|
|
131
|
+
# 走 env 覆盖 BAREAGENT_MEMORY_SEMANTIC_RECALL,restart-required。
|
|
132
|
+
# semantic_recall = false
|
|
133
|
+
# embedding backend:openai(复用 openai client,可指向独立 embeddings 端点)| local(fastembed,需装 [embeddings] extra)。
|
|
134
|
+
# embedding_backend = "openai"
|
|
135
|
+
# 模型,留空 = backend 默认(openai: text-embedding-3-small;local: BAAI/bge-small-en-v1.5)。
|
|
136
|
+
# embedding_model = ""
|
|
137
|
+
# openai backend 的端点/密钥,留空则复用会话 provider 的 base_url / api_key。
|
|
138
|
+
# embedding_base_url = ""
|
|
139
|
+
# embedding_api_key = ""
|
|
140
|
+
|
|
141
|
+
# --- Hooks (工具调用前后用户自定义钩子) ----------------------------------
|
|
142
|
+
# 在 [[hooks]] 中声明:在工具调用前后触发自定义 shell 命令。仅在主循环触发,
|
|
143
|
+
# 子代理不触发(隔离)。权限闸(PermissionGuard)仍是安全边界,hooks 是用户自配的
|
|
144
|
+
# 便利层(trust-the-config)。每个 hook 收到一行 JSON stdin(字段对齐 Claude Code):
|
|
145
|
+
# PreToolUse: {"event","tool_name","tool_input","session_id","cwd"}
|
|
146
|
+
# PostToolUse: 追加 {"tool_output","is_error"}
|
|
147
|
+
# 控制协议(exit code,对齐 Claude Code):
|
|
148
|
+
# - PreToolUse exit 2 = 拦截:跳过 handler,stderr 作拒绝理由回灌 LLM(error result)。
|
|
149
|
+
# - exit 0 = 放行;其他非 0 = 非阻塞警告 + 放行。
|
|
150
|
+
# - PostToolUse 退出码不影响工具结果(仅非 0 时警告)。
|
|
151
|
+
# 失败模式 = fail-open:hook spawn 失败 / 超时 → 警告 + 放行,不挂主循环。
|
|
152
|
+
# 字段:event(必填,PreToolUse|PostToolUse)、command(必填 shell 命令)、
|
|
153
|
+
# tool(可选,精确工具名;省略 = 匹配所有工具)、timeout(可选,秒,默认 30)。
|
|
154
|
+
# 默认全部注释掉(不启用任何 hook)。
|
|
155
|
+
#
|
|
156
|
+
# 注意:BareAgent 始终用 UTF-8 把 JSON 写入 hook 的 stdin。务必用
|
|
157
|
+
# json.load(sys.stdin.buffer)(二进制流 + 隐式 UTF-8 解码)读取,不要用
|
|
158
|
+
# json.load(sys.stdin)——后者按 OS locale 解码(中文 Windows 控制台是 GBK),
|
|
159
|
+
# 会把非 ASCII 的 tool_input(如中文文件路径)解成乱码。
|
|
160
|
+
|
|
161
|
+
# 示例 1:PreToolUse —— 挡危险,bash 命令含 "rm -rf" 时 exit 2 拦截。
|
|
162
|
+
# [[hooks]]
|
|
163
|
+
# event = "PreToolUse"
|
|
164
|
+
# tool = "bash"
|
|
165
|
+
# command = "python -c \"import sys,json; cmd=json.load(sys.stdin.buffer)['tool_input'].get('command',''); (sys.stderr.write('blocked: rm -rf'), sys.exit(2)) if 'rm -rf' in cmd else sys.exit(0)\""
|
|
166
|
+
|
|
167
|
+
# 示例 2:PostToolUse —— write_file 之后自动 ruff format 写入的文件。
|
|
168
|
+
# [[hooks]]
|
|
169
|
+
# event = "PostToolUse"
|
|
170
|
+
# tool = "write_file"
|
|
171
|
+
# command = "python -c \"import sys,json,subprocess; d=json.load(sys.stdin.buffer); p=d['tool_input'].get('file_path'); p and subprocess.run(['ruff','format',p])\""
|
|
172
|
+
# timeout = 60
|
|
173
|
+
|
|
174
|
+
[cost]
|
|
175
|
+
# Token 用量与成本估算(/cost 命令展示当前会话累计)。
|
|
176
|
+
# 默认内置 Claude Opus/Sonnet/Haiku 4.x 的参考价;其余 model 只显 token 不显 $。
|
|
177
|
+
# [cost.prices] 可覆盖内置价或为任意 model 新增价,单位均为「每百万 token 美元」。
|
|
178
|
+
# 价格可能变动,以 [cost.prices] 覆盖为准。
|
|
179
|
+
# [cost.prices."claude-opus-4-8"]
|
|
180
|
+
# input = 15.0
|
|
181
|
+
# output = 75.0
|
|
182
|
+
# [cost.prices."deepseek-chat"]
|
|
183
|
+
# input = 0.27
|
|
184
|
+
# output = 1.10
|
|
185
|
+
|
|
186
|
+
# --- Retry (LLM 调用重试策略) --------------------------------------------
|
|
187
|
+
# 瞬时性失败(rate limit / 网络超时 / 5xx / overloaded)自动指数退避重试;
|
|
188
|
+
# 不可重试错误(认证失败 / 400 bad request / 模型不存在 / 未知异常)立即上抛,
|
|
189
|
+
# 不掩盖真正的配置错误。app 层(src/core/retry.py)独占重试,已禁用 SDK 自带重试
|
|
190
|
+
# (anthropic / openai client 均 max_retries=0),避免 2×N 复合放大。
|
|
191
|
+
# 可重试状态码:408 / 409 / 429 / 500 / 502 / 503 / 504 / 529 + 连接/超时类异常。
|
|
192
|
+
# 不可重试状态码:400 / 401 / 403 / 404 / 413 / 422 + 未知异常(保守 fail-fast)。
|
|
193
|
+
# 退避:delay = min(max_delay_sec, base_delay_sec × multiplier^(attempt-1)),
|
|
194
|
+
# jitter=true 时再叠 full jitter(uniform(0, delay))。
|
|
195
|
+
# 环境变量覆盖:BAREAGENT_RETRY_ENABLED、BAREAGENT_RETRY_MAX_ATTEMPTS。
|
|
196
|
+
[retry]
|
|
197
|
+
# 关闭后完全无重试(含 SDK,因为 client 始终 max_retries=0)。
|
|
198
|
+
enabled = true
|
|
199
|
+
# 总尝试次数(含首次),<=1 等效关闭重试。
|
|
200
|
+
max_attempts = 3
|
|
201
|
+
# 首次重试前的基础等待秒数。
|
|
202
|
+
base_delay_sec = 1.0
|
|
203
|
+
# 单次退避上限秒数(封顶)。
|
|
204
|
+
max_delay_sec = 30.0
|
|
205
|
+
# 指数退避倍率。
|
|
206
|
+
multiplier = 2.0
|
|
207
|
+
# 是否叠加 full jitter(uniform(0, delay))打散并发重试。
|
|
208
|
+
jitter = true
|
|
209
|
+
|
|
210
|
+
# --- Prompt caching (Anthropic 显式 cache_control 断点) --------------------
|
|
211
|
+
# 给静态前缀(tools + system)和增长的对话历史前缀打 cache_control 断点,让多轮
|
|
212
|
+
# agent loop 反复重发的大块上下文走缓存读(约 0.1× 输入价),显著降本提速。
|
|
213
|
+
# 命名 provider 中立,但当前仅 Anthropic provider 生效(需显式打断点);
|
|
214
|
+
# OpenAI/DeepSeek 自动缓存、无请求侧旋钮,此开关对它们为 no-op(其命中量仍计入 /cost)。
|
|
215
|
+
# 关闭(enabled=false)时 Anthropic 请求体与未接缓存前字节级一致(向后兼容)。
|
|
216
|
+
# 注意:Opus 4.5+ 最小可缓存前缀为 4096 token,低于阈值静默 no-op(不报错)。
|
|
217
|
+
# 缓存读写 token 计入 /cost:写 1.25×(5m)、读 0.1×(claude)。
|
|
218
|
+
# 环境变量覆盖:BAREAGENT_CACHE_ENABLED。
|
|
219
|
+
# 改 [cache] 需重启生效(boot 时固化进 provider,不在 /reload 热重载集内)。
|
|
220
|
+
[cache]
|
|
221
|
+
# 是否启用 prompt caching(默认开启)。
|
|
222
|
+
enabled = true
|
|
223
|
+
# 缓存 TTL:"5m"(默认,交互式 agent 甜区)或 "1h"(写 2×,适合长时间挂起)。
|
|
224
|
+
ttl = "5m"
|
|
225
|
+
|
|
226
|
+
[skills]
|
|
227
|
+
# 经验式技能生成:复杂的多轮任务收尾后,agent 自动把工作流起草成可复用 skill
|
|
228
|
+
# 草稿(落 pending 区),用户用 /skill keep 提升到可加载。env 覆盖:
|
|
229
|
+
# BAREAGENT_SKILLS_AUTO_GENERATE。关闭即全链路短路(无额外反思 LLM 调用)。
|
|
230
|
+
auto_generate = true
|
|
231
|
+
# 触发为双条件 AND(自会话开始/上次起草后累计):工具调用数 + 用户回复数同时达标。
|
|
232
|
+
min_tool_calls = 5
|
|
233
|
+
min_user_replies = 3
|
|
234
|
+
# pending 草稿数量软上限,超出按最旧裁剪(<=0 关闭裁剪)。
|
|
235
|
+
max_pending = 10
|
|
236
|
+
# 生成 skill 根目录覆盖(留空 = ~/.bareagent/projects/<workspace-slug>/skills/,
|
|
237
|
+
# 与仓库 checked-in 的 skills/ 正典分离,项目隔离,不进版本控制)。
|
|
238
|
+
dir = ""
|
|
239
|
+
|
|
240
|
+
[goal]
|
|
241
|
+
# 完成条件循环:/goal [--max-turns N] <condition> 让 agent 自驱动一轮接一轮地工作,
|
|
242
|
+
# 每轮由一个独立评估器(只看对话 transcript)判定条件是否满足,未达标把理由回灌继续,
|
|
243
|
+
# 达标或触达 max_turns 即停。安全语义:/goal 尊重当前权限模式、绝不自动升级——
|
|
244
|
+
# DEFAULT 下写操作仍每轮弹确认(命令会提示用 /auto 切到无人值守);循环命令同步阻塞
|
|
245
|
+
# REPL,Esc 可中断。
|
|
246
|
+
# 轮数上限(防止评估器一直判不达标导致无限循环 + 失控成本)。env 覆盖:
|
|
247
|
+
# BAREAGENT_GOAL_MAX_TURNS;行内 --max-turns N 覆盖单次调用。
|
|
248
|
+
max_turns = 25
|
|
249
|
+
# 评估器模型(可选)。留空 = 复用会话 provider/模型(开箱即用、不依赖某家 provider);
|
|
250
|
+
# 填一个更便宜的 model id(如 claude-haiku-4-5)则经 factory 单独构造评估 provider,
|
|
251
|
+
# 省下每轮用主模型判定"是/否"的开销。boot 固化,改动 restart-required。
|
|
252
|
+
evaluator_model = ""
|
|
253
|
+
|
|
254
|
+
[workflow]
|
|
255
|
+
# 确定性多智能体编排(task 06-06-workflow-deterministic-orchestration,对标 Claude Code
|
|
256
|
+
# Workflow):LLM 经一个「主循环专属」的 workflow 工具临场产出一张静态 DAG(nodes +
|
|
257
|
+
# depends_on),相互独立的节点并发跑、依赖就绪才跑,上游产物经 {{node_id}} 占位喂给下游,
|
|
258
|
+
# 收尾把每个节点 done/failed/skipped + 产物结构化聚合回灌 LLM。节点执行复用 run_subagent。
|
|
259
|
+
# 安全语义:节点子代理 fail-closed 无人值守运行(worker 线程不弹确认,与后台子代理 / /loop
|
|
260
|
+
# 一脉相承)——DEFAULT 下写操作被拒、AUTO 下自动放行、PLAN 下只读;workflow 工具不进全局
|
|
261
|
+
# 工具集、子代理拿不到(无嵌套);同步阻塞调用、Esc 可中断。失败语义:单节点失败 = 其传递
|
|
262
|
+
# 下游 skip、独立分支继续。
|
|
263
|
+
# 扩展(task 06-08-workflow-background-panel-resume-budget):workflow 工具新增三个可选字段——
|
|
264
|
+
# run_in_background(后台 daemon 跑、立即返回 run id、完成后完整 summary 在下个 turn 回灌 LLM)、
|
|
265
|
+
# resume_from(给上次 run id,复用未变的已完成节点、只重跑变更/失败/新增节点及其下游)、
|
|
266
|
+
# token_budget(软上限,层间检查,超限后剩余节点 skip)。REPL 配套面板 /workflows(list | <run-id>
|
|
267
|
+
# | clear)。run 记录内存级、会话作用域(/new·/resume·/import clear、/compact 保留)。
|
|
268
|
+
# MVP 不做循环/条件/动态 fan-out、不做落盘跨重启 resume、不做取消运行中 workflow、不做
|
|
269
|
+
# worktree per-node 隔离(均为后续扩展位)。
|
|
270
|
+
# 总开关:false 时 workflow 工具压根不注入,整特性短路。env 覆盖:BAREAGENT_WORKFLOW_ENABLED。
|
|
271
|
+
enabled = true
|
|
272
|
+
# 并发上限:同时运行的节点数(每个节点是一个完整子代理,保守取值)。
|
|
273
|
+
max_concurrency = 8
|
|
274
|
+
# 单张 DAG 的节点数上限,防止 LLM 产出超大图打爆线程池。
|
|
275
|
+
max_nodes = 20
|
|
276
|
+
# 缺省 token 预算:workflow 调用未带 token_budget 时的兜底上限,0 = 不限;per-call 字段优先。
|
|
277
|
+
# env 覆盖:BAREAGENT_WORKFLOW_DEFAULT_TOKEN_BUDGET。
|
|
278
|
+
default_token_budget = 0
|
|
279
|
+
# 保留的 run 记录数(面板历史 + resume 来源)上限,FIFO 超出裁剪最旧(优先裁已完成的)。
|
|
280
|
+
# env 覆盖:BAREAGENT_WORKFLOW_MAX_RUNS。
|
|
281
|
+
max_runs = 50
|
|
282
|
+
|
|
283
|
+
[team]
|
|
284
|
+
# 多智能体队友协调(task 06-06-team-subsystem-completion):把 team 子系统补到
|
|
285
|
+
# 「LLM 真正能用的协作闭环」。team_send 现在阻塞等队友回复并把回复作为工具结果回灌 LLM
|
|
286
|
+
# (队友未运行 / 目标为 main 则立即返回,不干等超时);迟到 / 未经请求的回复经邮箱 drain
|
|
287
|
+
# 回灌到下一个 user turn。队友请求处理异常被隔离(回 error response、守护线程存活);
|
|
288
|
+
# team_shutdown <name> 可单独停一个队友;team_list 经后台线程存活探测反映真实运行状态。
|
|
289
|
+
# 安全语义:队友以 fail-closed 权限无人值守运行(与后台子代理 / /loop 一脉相承)。
|
|
290
|
+
# 两个字段均为 config-only 正数,boot 固化 -> 改动需重启(restart-required)。
|
|
291
|
+
# 队友守护循环空闲时每隔 poll_interval 秒醒来扫一次待办任务(收到消息会立即唤醒)。
|
|
292
|
+
poll_interval = 1.0
|
|
293
|
+
# 阻塞式 team_send 等待队友回复的超时秒数;超时返回提示,迟到回复仍会在后续 turn 浮现。
|
|
294
|
+
response_timeout = 60.0
|
|
295
|
+
# 队友有状态记忆(task 06-08-team-stateful-memory):开启后队友跨 request 保留对话上下文
|
|
296
|
+
# (连续 team_send 续在同一条会话上,注入 per-teammate Compactor 按 token 阈值压缩防无界增长);
|
|
297
|
+
# 自认领的 task 仍逐个无状态执行。关掉则回退到逐请求无状态的旧行为。
|
|
298
|
+
# 走 env 覆盖 BAREAGENT_TEAM_MEMORY_ENABLED,boot 固化 -> restart-required。
|
|
299
|
+
memory_enabled = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core runtime components for BareAgent."""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Resolution of the bundled config.toml and the user's local override.
|
|
2
|
+
|
|
3
|
+
config.toml ships *inside* the installed package (``src/bareagent/config.toml``)
|
|
4
|
+
as read-only defaults. The user's ``config.local.toml`` override, however, must
|
|
5
|
+
live somewhere writable:
|
|
6
|
+
|
|
7
|
+
* For the **bundled default** (read-only, inside the package / site-packages),
|
|
8
|
+
the override lives in the **current working directory** — so an installed user
|
|
9
|
+
can ``bareagent init`` / drop a ``config.local.toml`` next to where they run,
|
|
10
|
+
and a developer running from the repo root keeps picking up the repo's
|
|
11
|
+
``config.local.toml``.
|
|
12
|
+
* For an **explicit** ``--config`` / ``BAREAGENT_CONFIG`` path, the override is
|
|
13
|
+
the ``.local`` sibling of that file (unchanged historical behavior).
|
|
14
|
+
|
|
15
|
+
This module has no dependency on :mod:`bareagent.main`, so both ``main`` (load +
|
|
16
|
+
mtime watch) and :mod:`bareagent.provider.setup` (``init`` writer) can import it
|
|
17
|
+
without a circular import.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from importlib.resources import files
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def bundled_config_path() -> Path:
|
|
27
|
+
"""Filesystem path to the config.toml bundled inside the package.
|
|
28
|
+
|
|
29
|
+
importlib.resources resolves it for both editable installs (real source
|
|
30
|
+
tree) and wheel installs (site-packages). BareAgent is never a zipapp, so the
|
|
31
|
+
resource is always a real on-disk path safe to open later.
|
|
32
|
+
"""
|
|
33
|
+
return Path(str(files("bareagent").joinpath("config.toml")))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
DEFAULT_CONFIG_PATH = bundled_config_path()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def local_config_path(config_path: Path) -> Path:
|
|
40
|
+
"""Where the ``config.local.toml`` override lives for a given base config.
|
|
41
|
+
|
|
42
|
+
Bundled default -> current working directory; explicit path -> its ``.local``
|
|
43
|
+
sibling.
|
|
44
|
+
"""
|
|
45
|
+
if config_path == DEFAULT_CONFIG_PATH:
|
|
46
|
+
return Path.cwd() / "config.local.toml"
|
|
47
|
+
return config_path.with_suffix("").with_name(
|
|
48
|
+
config_path.stem + ".local" + config_path.suffix,
|
|
49
|
+
)
|