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,107 @@
|
|
|
1
|
+
"""OpenTelemetry tracer backend (optional dependency).
|
|
2
|
+
|
|
3
|
+
Requires the ``opentelemetry`` packages::
|
|
4
|
+
|
|
5
|
+
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
|
|
6
|
+
# or: pip install bareagent-cli[otel]
|
|
7
|
+
|
|
8
|
+
Activated automatically when ``OTEL_EXPORTER_OTLP_ENDPOINT`` is set, or when
|
|
9
|
+
``[tracing] opentelemetry = true`` appears in the config file.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import contextlib
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from bareagent.tracing._api import Span, Tracer
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OTelSpan(Span):
|
|
22
|
+
"""Span backed by an OpenTelemetry span."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, otel_span: Any) -> None:
|
|
25
|
+
self._span = otel_span
|
|
26
|
+
|
|
27
|
+
def set_tag(self, key: str, value: Any) -> None:
|
|
28
|
+
self._span.set_attribute(key, _coerce(value))
|
|
29
|
+
|
|
30
|
+
def set_content_tag(self, key: str, value: Any) -> None:
|
|
31
|
+
self._span.set_attribute(f"content.{key}", _coerce(value))
|
|
32
|
+
|
|
33
|
+
def set_error(self, error: str) -> None:
|
|
34
|
+
from opentelemetry.trace import StatusCode
|
|
35
|
+
|
|
36
|
+
self._span.set_status(StatusCode.ERROR, error)
|
|
37
|
+
|
|
38
|
+
def end(self) -> None:
|
|
39
|
+
self._span.end()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OpenTelemetryTracer(Tracer):
|
|
43
|
+
"""Tracer that emits standard OpenTelemetry spans.
|
|
44
|
+
|
|
45
|
+
Any OTel-compatible backend (Jaeger, Datadog, Langfuse via OTel, etc.)
|
|
46
|
+
can consume the spans.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, service_name: str = "bareagent") -> None:
|
|
50
|
+
from opentelemetry import trace
|
|
51
|
+
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
|
52
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
53
|
+
|
|
54
|
+
resource = Resource.create({SERVICE_NAME: service_name})
|
|
55
|
+
provider = TracerProvider(resource=resource)
|
|
56
|
+
trace.set_tracer_provider(provider)
|
|
57
|
+
self._provider = provider
|
|
58
|
+
self._tracer = trace.get_tracer("bareagent")
|
|
59
|
+
self._current: OTelSpan | None = None
|
|
60
|
+
|
|
61
|
+
def add_exporter(self, exporter: Any, *, batch: bool = True) -> None:
|
|
62
|
+
"""Attach a span exporter to the provider."""
|
|
63
|
+
from opentelemetry.sdk.trace.export import (
|
|
64
|
+
BatchSpanProcessor,
|
|
65
|
+
SimpleSpanProcessor,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
processor = (
|
|
69
|
+
BatchSpanProcessor(exporter) if batch else SimpleSpanProcessor(exporter)
|
|
70
|
+
)
|
|
71
|
+
self._provider.add_span_processor(processor)
|
|
72
|
+
|
|
73
|
+
@contextlib.contextmanager
|
|
74
|
+
def trace(
|
|
75
|
+
self,
|
|
76
|
+
operation_name: str,
|
|
77
|
+
tags: dict[str, Any] | None = None,
|
|
78
|
+
*,
|
|
79
|
+
parent_span: Span | None = None,
|
|
80
|
+
) -> Iterator[Span]:
|
|
81
|
+
with self._tracer.start_as_current_span(operation_name) as raw_span:
|
|
82
|
+
span = OTelSpan(raw_span)
|
|
83
|
+
if tags:
|
|
84
|
+
for k, v in tags.items():
|
|
85
|
+
span.set_tag(k, v)
|
|
86
|
+
prev = self._current
|
|
87
|
+
self._current = span
|
|
88
|
+
try:
|
|
89
|
+
yield span
|
|
90
|
+
finally:
|
|
91
|
+
self._current = prev
|
|
92
|
+
|
|
93
|
+
def current_span(self) -> Span | None:
|
|
94
|
+
return self._current
|
|
95
|
+
|
|
96
|
+
def flush(self) -> None:
|
|
97
|
+
self._provider.force_flush()
|
|
98
|
+
|
|
99
|
+
def shutdown(self) -> None:
|
|
100
|
+
self._provider.shutdown()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _coerce(value: Any) -> str | int | float | bool:
|
|
104
|
+
"""Coerce a value to an OTel-compatible attribute type."""
|
|
105
|
+
if isinstance(value, (str, int, float, bool)):
|
|
106
|
+
return value
|
|
107
|
+
return str(value)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Configure tracing from config + environment (Haystack auto-detect pattern)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from bareagent.tracing._api import Tracer
|
|
9
|
+
from bareagent.tracing._proxy import enable_tracing
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def configure_tracing(
|
|
13
|
+
tracing_config: Any,
|
|
14
|
+
*,
|
|
15
|
+
session_id: str = "default",
|
|
16
|
+
interaction_logger: Any = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Read ``[tracing]`` config and wire up the global tracer.
|
|
19
|
+
|
|
20
|
+
Backends are activated by configuration or environment variables:
|
|
21
|
+
|
|
22
|
+
- **JsonFile**: always active when *interaction_logger* is provided
|
|
23
|
+
(backward compat with ``/log`` and the debug web viewer).
|
|
24
|
+
- **Langfuse**: ``LANGFUSE_PUBLIC_KEY`` env var, or
|
|
25
|
+
``[tracing] langfuse = true``.
|
|
26
|
+
- **OpenTelemetry**: ``OTEL_EXPORTER_OTLP_ENDPOINT`` env var, or
|
|
27
|
+
``[tracing] opentelemetry = true``.
|
|
28
|
+
|
|
29
|
+
When multiple backends are active a :class:`CompositeTracer` fans
|
|
30
|
+
out to all of them.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
backends: list[Tracer] = []
|
|
34
|
+
|
|
35
|
+
# 1) JsonFile backend (always on if interaction_logger provided)
|
|
36
|
+
if interaction_logger is not None:
|
|
37
|
+
from bareagent.tracing.json_file import JsonFileTracer
|
|
38
|
+
|
|
39
|
+
backends.append(JsonFileTracer(interaction_logger))
|
|
40
|
+
|
|
41
|
+
# 2) Langfuse (config or env-var driven)
|
|
42
|
+
if _langfuse_enabled(tracing_config):
|
|
43
|
+
try:
|
|
44
|
+
from bareagent.tracing.langfuse import LangfuseTracer
|
|
45
|
+
|
|
46
|
+
backends.append(LangfuseTracer(session_id=session_id))
|
|
47
|
+
except ImportError:
|
|
48
|
+
pass # langfuse not installed
|
|
49
|
+
|
|
50
|
+
# 3) OpenTelemetry (auto-detect: if OTEL_EXPORTER_OTLP_ENDPOINT is set)
|
|
51
|
+
if _otel_enabled(tracing_config):
|
|
52
|
+
try:
|
|
53
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
54
|
+
OTLPSpanExporter,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
from bareagent.tracing.otel import OpenTelemetryTracer
|
|
58
|
+
|
|
59
|
+
otel = OpenTelemetryTracer()
|
|
60
|
+
otel.add_exporter(OTLPSpanExporter())
|
|
61
|
+
backends.append(otel)
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass # opentelemetry not installed
|
|
64
|
+
|
|
65
|
+
if not backends:
|
|
66
|
+
return # NullTracer remains active
|
|
67
|
+
|
|
68
|
+
if len(backends) == 1:
|
|
69
|
+
enable_tracing(backends[0])
|
|
70
|
+
else:
|
|
71
|
+
from bareagent.tracing.composite import CompositeTracer
|
|
72
|
+
|
|
73
|
+
enable_tracing(CompositeTracer(backends))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _langfuse_enabled(config: Any) -> bool:
|
|
77
|
+
if os.getenv("LANGFUSE_PUBLIC_KEY"):
|
|
78
|
+
return True
|
|
79
|
+
return bool(getattr(config, "langfuse", False))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _otel_enabled(config: Any) -> bool:
|
|
83
|
+
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
|
|
84
|
+
return True
|
|
85
|
+
return bool(getattr(config, "opentelemetry", False))
|
bareagent/ui/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""UI modules for BareAgent."""
|
|
2
|
+
|
|
3
|
+
from bareagent.ui.console import AgentConsole
|
|
4
|
+
from bareagent.ui.protocol import StreamProtocol, UIProtocol
|
|
5
|
+
from bareagent.ui.stream import StreamPrinter
|
|
6
|
+
from bareagent.ui.theme import (
|
|
7
|
+
ThemeManager,
|
|
8
|
+
format_theme_list,
|
|
9
|
+
format_unknown_theme,
|
|
10
|
+
get_theme,
|
|
11
|
+
init_theme,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AgentConsole",
|
|
16
|
+
"StreamPrinter",
|
|
17
|
+
"UIProtocol",
|
|
18
|
+
"StreamProtocol",
|
|
19
|
+
"ThemeManager",
|
|
20
|
+
"get_theme",
|
|
21
|
+
"init_theme",
|
|
22
|
+
"format_theme_list",
|
|
23
|
+
"format_unknown_theme",
|
|
24
|
+
]
|
bareagent/ui/console.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from rich import box
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.markdown import Markdown
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.syntax import Syntax
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from bareagent.ui.theme import ThemeManager, get_theme
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from bareagent.ui.stream import StreamPrinter
|
|
17
|
+
|
|
18
|
+
MAX_TOOL_OUTPUT_CHARS = 2000
|
|
19
|
+
MAX_PERMISSION_PREVIEW_CHARS = 500
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AgentConsole:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
console: Console | None = None,
|
|
26
|
+
theme: ThemeManager | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
tm = theme or get_theme()
|
|
29
|
+
self.console = console or Console(
|
|
30
|
+
no_color=tm.no_color,
|
|
31
|
+
)
|
|
32
|
+
self._permission_choice: str | None = None
|
|
33
|
+
self._theme_pushed = False
|
|
34
|
+
self.set_theme(tm)
|
|
35
|
+
|
|
36
|
+
def set_theme(self, theme: ThemeManager | None = None) -> None:
|
|
37
|
+
tm = theme or get_theme()
|
|
38
|
+
if self._theme_pushed:
|
|
39
|
+
self.console.pop_theme()
|
|
40
|
+
self.console.push_theme(tm.rich_theme)
|
|
41
|
+
self._theme_pushed = True
|
|
42
|
+
|
|
43
|
+
def print_assistant(self, text: str) -> None:
|
|
44
|
+
if not text.strip():
|
|
45
|
+
return
|
|
46
|
+
self.console.print(Markdown(text))
|
|
47
|
+
|
|
48
|
+
def print_tool_call(self, name: str, input_data: Any) -> None:
|
|
49
|
+
icons = get_theme().icons
|
|
50
|
+
code, lexer = _render_payload(input_data)
|
|
51
|
+
self.console.print(
|
|
52
|
+
Panel(
|
|
53
|
+
Syntax(code, lexer, word_wrap=True),
|
|
54
|
+
title=f"[tool.name]{icons.tool} Tool Call[/] [muted]{name}[/]",
|
|
55
|
+
border_style="tool.border",
|
|
56
|
+
box=box.ROUNDED,
|
|
57
|
+
padding=(0, 1),
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def print_tool_result(self, name: str, output: Any) -> None:
|
|
62
|
+
icons = get_theme().icons
|
|
63
|
+
code, lexer = _render_payload(output, max_chars=MAX_TOOL_OUTPUT_CHARS)
|
|
64
|
+
self.console.print(
|
|
65
|
+
Panel(
|
|
66
|
+
Syntax(code, lexer, word_wrap=True),
|
|
67
|
+
title=f"[success]{icons.success} Result[/] [muted]{name}[/]",
|
|
68
|
+
border_style="result.border",
|
|
69
|
+
box=box.ROUNDED,
|
|
70
|
+
padding=(0, 1),
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def ask_permission(self, name: str, input_data: Any) -> bool:
|
|
75
|
+
theme = get_theme()
|
|
76
|
+
self._permission_choice = None
|
|
77
|
+
self.console.print(
|
|
78
|
+
Panel(
|
|
79
|
+
Syntax(
|
|
80
|
+
_render_permission_payload(input_data),
|
|
81
|
+
"json",
|
|
82
|
+
word_wrap=True,
|
|
83
|
+
),
|
|
84
|
+
title=(
|
|
85
|
+
f"[warning]{theme.icons.warning} Permission Required: {name}[/]"
|
|
86
|
+
),
|
|
87
|
+
subtitle=Text("[y] Allow [n] Deny [a] Always allow"),
|
|
88
|
+
border_style=theme.palette.warning,
|
|
89
|
+
box=box.ROUNDED,
|
|
90
|
+
padding=(0, 1),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
while True:
|
|
95
|
+
try:
|
|
96
|
+
choice = input("> ").strip().lower()
|
|
97
|
+
except (EOFError, KeyboardInterrupt):
|
|
98
|
+
self._permission_choice = "deny"
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
if choice == "y":
|
|
102
|
+
self._permission_choice = "allow"
|
|
103
|
+
return True
|
|
104
|
+
if choice == "n":
|
|
105
|
+
self._permission_choice = "deny"
|
|
106
|
+
return False
|
|
107
|
+
if choice == "a":
|
|
108
|
+
self._permission_choice = "always"
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
self.console.print("Press y/n/a", style="permission.ask")
|
|
112
|
+
|
|
113
|
+
def consume_permission_choice(self) -> str | None:
|
|
114
|
+
choice = self._permission_choice
|
|
115
|
+
self._permission_choice = None
|
|
116
|
+
return choice
|
|
117
|
+
|
|
118
|
+
def print_error(self, msg: str) -> None:
|
|
119
|
+
icons = get_theme().icons
|
|
120
|
+
self.console.print(f"{icons.error} {msg}", style="error")
|
|
121
|
+
|
|
122
|
+
def print_status(self, msg: str) -> None:
|
|
123
|
+
self.console.print(msg, style="status")
|
|
124
|
+
|
|
125
|
+
def get_stream_printer(self) -> StreamPrinter:
|
|
126
|
+
from bareagent.ui.stream import StreamPrinter
|
|
127
|
+
|
|
128
|
+
return StreamPrinter(self.console)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _render_payload(payload: Any, *, max_chars: int | None = None) -> tuple[str, str]:
|
|
132
|
+
if isinstance(payload, (dict, list, tuple)):
|
|
133
|
+
text = json.dumps(payload, ensure_ascii=False, indent=2, default=str)
|
|
134
|
+
return _truncate(text, max_chars), "json"
|
|
135
|
+
|
|
136
|
+
if payload is None:
|
|
137
|
+
return "", "text"
|
|
138
|
+
|
|
139
|
+
text = str(payload)
|
|
140
|
+
parsed = _try_parse_json(text)
|
|
141
|
+
if parsed is not None:
|
|
142
|
+
# Re-format for consistent indentation.
|
|
143
|
+
formatted = json.dumps(parsed, ensure_ascii=False, indent=2, default=str)
|
|
144
|
+
return _truncate(formatted, max_chars), "json"
|
|
145
|
+
return _truncate(text, max_chars), "text"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _render_permission_payload(payload: Any) -> str:
|
|
149
|
+
text, _ = _render_payload(payload, max_chars=MAX_PERMISSION_PREVIEW_CHARS)
|
|
150
|
+
return text
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _truncate(text: str, max_chars: int | None) -> str:
|
|
154
|
+
if max_chars is None or len(text) <= max_chars:
|
|
155
|
+
return text
|
|
156
|
+
return f"{text[:max_chars]}\n\n... [truncated]"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _try_parse_json(text: str) -> Any:
|
|
160
|
+
"""Return parsed JSON object if *text* looks like JSON, else ``None``."""
|
|
161
|
+
stripped = text.strip()
|
|
162
|
+
if not stripped or stripped[0] not in "{[":
|
|
163
|
+
return None
|
|
164
|
+
try:
|
|
165
|
+
return json.loads(stripped)
|
|
166
|
+
except json.JSONDecodeError:
|
|
167
|
+
return None
|
bareagent/ui/prompt.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit import PromptSession
|
|
7
|
+
from prompt_toolkit.completion import WordCompleter
|
|
8
|
+
from prompt_toolkit.history import FileHistory, History, InMemoryHistory
|
|
9
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _ResilientFileHistory(FileHistory):
|
|
13
|
+
def store_string(self, string: str) -> None:
|
|
14
|
+
try:
|
|
15
|
+
super().store_string(string)
|
|
16
|
+
except OSError:
|
|
17
|
+
# Keep the current session usable even if history persistence fails.
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentPrompt:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
commands: list[str],
|
|
26
|
+
history_file: Path,
|
|
27
|
+
get_mode_label: Callable[[], str],
|
|
28
|
+
cycle_mode: Callable[[], str] | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
self._get_mode_label = get_mode_label
|
|
31
|
+
self._cycle_mode = cycle_mode
|
|
32
|
+
self._session = PromptSession(
|
|
33
|
+
completer=WordCompleter(commands, sentence=True),
|
|
34
|
+
history=self._build_history(history_file),
|
|
35
|
+
key_bindings=self._build_bindings(cycle_mode),
|
|
36
|
+
complete_while_typing=True,
|
|
37
|
+
bottom_toolbar=self._toolbar,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def read_input(self) -> str:
|
|
41
|
+
return self._session.prompt(
|
|
42
|
+
lambda: f"[{self._get_mode_label()}] bareagent> ",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _toolbar(self) -> str:
|
|
46
|
+
mode = self._get_mode_label()
|
|
47
|
+
if self._cycle_mode is None:
|
|
48
|
+
return f" Mode: {mode} | /help: commands"
|
|
49
|
+
return f" Mode: {mode} | Shift+Tab: cycle mode | /help: commands"
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _build_history(history_file: Path) -> History:
|
|
53
|
+
try:
|
|
54
|
+
history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
with history_file.open("ab"):
|
|
56
|
+
pass
|
|
57
|
+
except OSError:
|
|
58
|
+
return InMemoryHistory()
|
|
59
|
+
return _ResilientFileHistory(str(history_file))
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _build_bindings(
|
|
63
|
+
cycle_mode: Callable[[], str] | None = None,
|
|
64
|
+
) -> KeyBindings:
|
|
65
|
+
bindings = KeyBindings()
|
|
66
|
+
|
|
67
|
+
@bindings.add("c-j")
|
|
68
|
+
def _insert_newline(event) -> None:
|
|
69
|
+
event.current_buffer.insert_text("\n")
|
|
70
|
+
|
|
71
|
+
@bindings.add("s-tab")
|
|
72
|
+
def _on_cycle_mode(event) -> None:
|
|
73
|
+
if cycle_mode is None:
|
|
74
|
+
return
|
|
75
|
+
cycle_mode()
|
|
76
|
+
event.app.invalidate()
|
|
77
|
+
|
|
78
|
+
return bindings
|
bareagent/ui/protocol.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class StreamProtocol(Protocol):
|
|
8
|
+
"""Streaming output abstraction."""
|
|
9
|
+
|
|
10
|
+
def start(self) -> None: ...
|
|
11
|
+
def feed(self, token: str) -> None: ...
|
|
12
|
+
def finish(self) -> str: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class UIProtocol(Protocol):
|
|
17
|
+
"""UI abstraction consumed by the agent loop."""
|
|
18
|
+
|
|
19
|
+
def print_assistant(self, text: str) -> None: ...
|
|
20
|
+
def print_tool_call(self, name: str, input_data: Any) -> None: ...
|
|
21
|
+
def print_tool_result(self, name: str, output: Any) -> None: ...
|
|
22
|
+
def print_error(self, msg: str) -> None: ...
|
|
23
|
+
def print_status(self, msg: str) -> None: ...
|
|
24
|
+
def get_stream_printer(self) -> StreamProtocol: ...
|
bareagent/ui/stream.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import TextIO
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from bareagent.ui.theme import get_theme
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StreamPrinter:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
console: Console | None = None,
|
|
15
|
+
*,
|
|
16
|
+
status_message: str = "Thinking...",
|
|
17
|
+
writer: TextIO | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
tm = get_theme()
|
|
20
|
+
self.console = console or Console(
|
|
21
|
+
no_color=tm.no_color,
|
|
22
|
+
)
|
|
23
|
+
self.status_message = status_message
|
|
24
|
+
console_writer = getattr(self.console, "file", None)
|
|
25
|
+
self.writer = writer or console_writer or sys.stdout
|
|
26
|
+
self._chunks: list[str] = []
|
|
27
|
+
self._active = False
|
|
28
|
+
self._printed_anything = False
|
|
29
|
+
self._theme_pushed = False
|
|
30
|
+
|
|
31
|
+
def start(self) -> None:
|
|
32
|
+
if self._active:
|
|
33
|
+
return
|
|
34
|
+
tm = get_theme()
|
|
35
|
+
if not self._theme_pushed:
|
|
36
|
+
self.console.push_theme(tm.rich_theme)
|
|
37
|
+
self._theme_pushed = True
|
|
38
|
+
if not self._chunks:
|
|
39
|
+
self.console.print(
|
|
40
|
+
f"{tm.icons.running} {self.status_message}",
|
|
41
|
+
style="status",
|
|
42
|
+
)
|
|
43
|
+
self._active = True
|
|
44
|
+
|
|
45
|
+
def feed(self, token: str) -> None:
|
|
46
|
+
if not token:
|
|
47
|
+
return
|
|
48
|
+
if not self._active:
|
|
49
|
+
self.start()
|
|
50
|
+
self._chunks.append(token)
|
|
51
|
+
self.writer.write(token)
|
|
52
|
+
self.writer.flush()
|
|
53
|
+
self._printed_anything = True
|
|
54
|
+
|
|
55
|
+
def finish(self) -> str:
|
|
56
|
+
if self._active and self._printed_anything:
|
|
57
|
+
self.writer.write("\n")
|
|
58
|
+
self.writer.flush()
|
|
59
|
+
self._active = False
|
|
60
|
+
if self._theme_pushed:
|
|
61
|
+
self.console.pop_theme()
|
|
62
|
+
self._theme_pushed = False
|
|
63
|
+
result = "".join(self._chunks)
|
|
64
|
+
self._chunks = []
|
|
65
|
+
self._printed_anything = False
|
|
66
|
+
return result
|