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,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))
@@ -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
+ ]
@@ -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
@@ -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