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,129 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from enum import Enum
6
+
7
+ from bareagent.team.mailbox import Message, MessageBus
8
+
9
+
10
+ class Protocol(Enum):
11
+ PLAN_APPROVAL = "plan_approval"
12
+ SHUTDOWN = "shutdown"
13
+
14
+
15
+ class ProtocolFSM:
16
+ """Simple polling request-response helper built on MessageBus."""
17
+
18
+ def __init__(self, bus: MessageBus, agent_name: str) -> None:
19
+ self.bus = bus
20
+ self.agent_name = agent_name
21
+ self.bus.ensure_mailbox(agent_name)
22
+
23
+ def request(self, to: str, protocol: Protocol, content: str) -> str:
24
+ return self.bus.send(
25
+ Message(
26
+ id="",
27
+ from_agent=self.agent_name,
28
+ to_agent=to,
29
+ content=encode_protocol_content(protocol, content),
30
+ msg_type="request",
31
+ timestamp="",
32
+ )
33
+ )
34
+
35
+ def wait_response(self, msg_id: str, timeout: float = 60) -> Message | None:
36
+ deadline = time.monotonic() + timeout
37
+ cursor: str | None = None
38
+
39
+ while time.monotonic() < deadline:
40
+ messages = self.bus.receive(self.agent_name, since_id=cursor)
41
+ if messages:
42
+ cursor = messages[-1].id
43
+ for message in messages:
44
+ if message.msg_type != "response":
45
+ continue
46
+ if message.in_reply_to == msg_id:
47
+ return message
48
+ remaining = deadline - time.monotonic()
49
+ if remaining <= 0:
50
+ break
51
+ self.bus.wait_for_message(self.agent_name, timeout=min(remaining, 0.5))
52
+
53
+ return None
54
+
55
+ def respond(self, in_reply_to: str, content: str) -> str:
56
+ request_message = self.bus.find_message(in_reply_to)
57
+ if request_message is None:
58
+ raise ValueError(f"Unknown request message id: {in_reply_to}")
59
+
60
+ protocol, _ = decode_protocol_content(request_message.content)
61
+ response_content = (
62
+ encode_protocol_content(protocol, content)
63
+ if protocol is not None
64
+ else content
65
+ )
66
+ return self.bus.send(
67
+ Message(
68
+ id="",
69
+ from_agent=self.agent_name,
70
+ to_agent=request_message.from_agent,
71
+ content=response_content,
72
+ msg_type="response",
73
+ timestamp="",
74
+ in_reply_to=in_reply_to,
75
+ )
76
+ )
77
+
78
+ def broadcast(self, protocol: Protocol, content: str) -> list[str]:
79
+ recipients = [
80
+ agent_name
81
+ for agent_name in self.bus.list_agents()
82
+ if agent_name != self.agent_name
83
+ ]
84
+ message_ids: list[str] = []
85
+ for recipient in recipients:
86
+ message_ids.append(
87
+ self.bus.send(
88
+ Message(
89
+ id="",
90
+ from_agent=self.agent_name,
91
+ to_agent=recipient,
92
+ content=encode_protocol_content(protocol, content),
93
+ msg_type="broadcast",
94
+ timestamp="",
95
+ )
96
+ )
97
+ )
98
+ return message_ids
99
+
100
+
101
+ def encode_protocol_content(protocol: Protocol, content: str) -> str:
102
+ return json.dumps(
103
+ {
104
+ "protocol": protocol.value,
105
+ "content": content,
106
+ },
107
+ ensure_ascii=False,
108
+ )
109
+
110
+
111
+ def decode_protocol_content(content: str) -> tuple[Protocol | None, str]:
112
+ try:
113
+ payload = json.loads(content)
114
+ except (TypeError, json.JSONDecodeError):
115
+ return None, str(content)
116
+
117
+ if not isinstance(payload, dict):
118
+ return None, str(content)
119
+
120
+ protocol_name = payload.get("protocol")
121
+ body = payload.get("content", "")
122
+ if not isinstance(protocol_name, str):
123
+ return None, str(content)
124
+
125
+ try:
126
+ protocol = Protocol(protocol_name)
127
+ except ValueError:
128
+ return None, str(content)
129
+ return protocol, str(body)
@@ -0,0 +1,12 @@
1
+ from bareagent.tracing._api import NullSpan, NullTracer, Span, Tracer
2
+ from bareagent.tracing._proxy import ProxyTracer, enable_tracing, tracer
3
+
4
+ __all__ = [
5
+ "NullSpan",
6
+ "NullTracer",
7
+ "Span",
8
+ "Tracer",
9
+ "ProxyTracer",
10
+ "enable_tracing",
11
+ "tracer",
12
+ ]
@@ -0,0 +1,92 @@
1
+ """Tracing abstractions: Span and Tracer ABCs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import abc
6
+ import contextlib
7
+ from collections.abc import Iterator
8
+ from typing import Any
9
+
10
+
11
+ class Span(abc.ABC):
12
+ """A single instrumented operation."""
13
+
14
+ @abc.abstractmethod
15
+ def set_tag(self, key: str, value: Any) -> None:
16
+ """Attach metadata (model name, tool name, etc.)."""
17
+
18
+ @abc.abstractmethod
19
+ def set_content_tag(self, key: str, value: Any) -> None:
20
+ """Attach content-sensitive data (messages, outputs).
21
+
22
+ Backends may skip these if content tracing is disabled.
23
+ """
24
+
25
+ @abc.abstractmethod
26
+ def set_error(self, error: str) -> None:
27
+ """Mark the span as failed."""
28
+
29
+ @abc.abstractmethod
30
+ def end(self) -> None:
31
+ """Finalize the span."""
32
+
33
+
34
+ class Tracer(abc.ABC):
35
+ """Interface for instrumenting code with spans."""
36
+
37
+ @abc.abstractmethod
38
+ @contextlib.contextmanager
39
+ def trace(
40
+ self,
41
+ operation_name: str,
42
+ tags: dict[str, Any] | None = None,
43
+ *,
44
+ parent_span: Span | None = None,
45
+ ) -> Iterator[Span]:
46
+ """Create a span for the given operation."""
47
+
48
+ @abc.abstractmethod
49
+ def current_span(self) -> Span | None:
50
+ """Return the currently active span, if any."""
51
+
52
+ # flush/shutdown are optional no-op hooks; subclasses override as needed.
53
+ def flush(self) -> None: # noqa: B027
54
+ """Flush pending data to the backend. No-op by default."""
55
+
56
+ def shutdown(self) -> None: # noqa: B027
57
+ """Release resources. No-op by default."""
58
+
59
+
60
+ class NullSpan(Span):
61
+ """Zero-overhead no-op span."""
62
+
63
+ def set_tag(self, key: str, value: Any) -> None:
64
+ pass
65
+
66
+ def set_content_tag(self, key: str, value: Any) -> None:
67
+ pass
68
+
69
+ def set_error(self, error: str) -> None:
70
+ pass
71
+
72
+ def end(self) -> None:
73
+ pass
74
+
75
+
76
+ class NullTracer(Tracer):
77
+ """Zero-overhead no-op tracer."""
78
+
79
+ _NULL_SPAN = NullSpan()
80
+
81
+ @contextlib.contextmanager
82
+ def trace(
83
+ self,
84
+ operation_name: str,
85
+ tags: dict[str, Any] | None = None,
86
+ *,
87
+ parent_span: Span | None = None,
88
+ ) -> Iterator[Span]:
89
+ yield self._NULL_SPAN
90
+
91
+ def current_span(self) -> Span | None:
92
+ return None
@@ -0,0 +1,60 @@
1
+ """Global proxy tracer -- hot-swappable at runtime (Haystack pattern)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import os
7
+ import threading
8
+ from collections.abc import Iterator
9
+ from typing import Any
10
+
11
+ from bareagent.tracing._api import NullTracer, Span, Tracer
12
+
13
+
14
+ class ProxyTracer(Tracer):
15
+ """Delegates to an inner tracer that can be replaced at runtime."""
16
+
17
+ def __init__(self, inner: Tracer | None = None) -> None:
18
+ self._inner: Tracer = inner or NullTracer()
19
+ self._lock = threading.Lock()
20
+ self.is_content_tracing_enabled: bool = os.getenv(
21
+ "BAREAGENT_CONTENT_TRACING_ENABLED", "true"
22
+ ).lower() in {"1", "true", "yes", "on"}
23
+
24
+ @property
25
+ def inner(self) -> Tracer:
26
+ return self._inner
27
+
28
+ @inner.setter
29
+ def inner(self, tracer: Tracer) -> None:
30
+ with self._lock:
31
+ self._inner = tracer
32
+
33
+ @contextlib.contextmanager
34
+ def trace(
35
+ self,
36
+ operation_name: str,
37
+ tags: dict[str, Any] | None = None,
38
+ *,
39
+ parent_span: Span | None = None,
40
+ ) -> Iterator[Span]:
41
+ with self._inner.trace(operation_name, tags, parent_span=parent_span) as span:
42
+ yield span
43
+
44
+ def current_span(self) -> Span | None:
45
+ return self._inner.current_span()
46
+
47
+ def flush(self) -> None:
48
+ self._inner.flush()
49
+
50
+ def shutdown(self) -> None:
51
+ self._inner.shutdown()
52
+
53
+
54
+ # Global singleton -- the only import any module needs.
55
+ tracer: ProxyTracer = ProxyTracer()
56
+
57
+
58
+ def enable_tracing(provided_tracer: Tracer) -> None:
59
+ """Replace the global tracer backend at runtime."""
60
+ tracer.inner = provided_tracer
@@ -0,0 +1,115 @@
1
+ """Fan-out tracer that delegates to N backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from collections.abc import Iterator
7
+ from typing import Any
8
+
9
+ from bareagent.tracing._api import Span, Tracer
10
+
11
+
12
+ class CompositeSpan(Span):
13
+ """Span that fans out to multiple backend spans."""
14
+
15
+ def __init__(self, spans: list[Span]) -> None:
16
+ self._spans = spans
17
+
18
+ def set_tag(self, key: str, value: Any) -> None:
19
+ for span in self._spans:
20
+ try:
21
+ span.set_tag(key, value)
22
+ except Exception:
23
+ pass
24
+
25
+ def set_content_tag(self, key: str, value: Any) -> None:
26
+ for span in self._spans:
27
+ try:
28
+ span.set_content_tag(key, value)
29
+ except Exception:
30
+ pass
31
+
32
+ def set_error(self, error: str) -> None:
33
+ for span in self._spans:
34
+ try:
35
+ span.set_error(error)
36
+ except Exception:
37
+ pass
38
+
39
+ def end(self) -> None:
40
+ for span in self._spans:
41
+ try:
42
+ span.end()
43
+ except Exception:
44
+ pass
45
+
46
+
47
+ class CompositeTracer(Tracer):
48
+ """Tracer that fans out to N backends simultaneously."""
49
+
50
+ def __init__(self, tracers: list[Tracer]) -> None:
51
+ self._tracers = list(tracers)
52
+
53
+ @contextlib.contextmanager
54
+ def trace(
55
+ self,
56
+ operation_name: str,
57
+ tags: dict[str, Any] | None = None,
58
+ *,
59
+ parent_span: Span | None = None,
60
+ ) -> Iterator[Span]:
61
+ spans: list[Span] = []
62
+ exits: list[contextlib.AbstractContextManager[Span]] = []
63
+ try:
64
+ for tracer in self._tracers:
65
+ cm = tracer.trace(operation_name, tags, parent_span=parent_span)
66
+ span = cm.__enter__()
67
+ spans.append(span)
68
+ exits.append(cm)
69
+ yield CompositeSpan(spans)
70
+ finally:
71
+ for cm in reversed(exits):
72
+ try:
73
+ cm.__exit__(None, None, None)
74
+ except Exception:
75
+ pass
76
+
77
+ def current_span(self) -> Span | None:
78
+ for tracer in self._tracers:
79
+ span = tracer.current_span()
80
+ if span is not None:
81
+ return span
82
+ return None
83
+
84
+ def flush(self) -> None:
85
+ for tracer in self._tracers:
86
+ try:
87
+ tracer.flush()
88
+ except Exception:
89
+ pass
90
+
91
+ def shutdown(self) -> None:
92
+ for tracer in self._tracers:
93
+ try:
94
+ tracer.shutdown()
95
+ except Exception:
96
+ pass
97
+
98
+ # ---- Delegate InteractionLogger query methods to first JsonFileTracer ----
99
+
100
+ def _json_file_tracer(self) -> Any:
101
+ from bareagent.tracing.json_file import JsonFileTracer
102
+
103
+ for tracer in self._tracers:
104
+ if isinstance(tracer, JsonFileTracer):
105
+ return tracer
106
+ return None
107
+
108
+ def __getattr__(self, name: str) -> Any:
109
+ """Forward InteractionLogger methods to the first JsonFileTracer."""
110
+ jft = self._json_file_tracer()
111
+ if jft is not None:
112
+ attr = getattr(jft, name, None)
113
+ if attr is not None:
114
+ return attr
115
+ raise AttributeError(name)
@@ -0,0 +1,115 @@
1
+ """JSON file tracer -- wraps InteractionLogger via composition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from collections.abc import Iterator
7
+ from typing import Any
8
+
9
+ from bareagent.debug.interaction_log import InteractionLogger
10
+ from bareagent.tracing._api import Span, Tracer
11
+
12
+
13
+ class JsonFileSpan(Span):
14
+ """Span that accumulates tags for the JSON file backend."""
15
+
16
+ def __init__(self, operation_name: str) -> None:
17
+ self.operation_name = operation_name
18
+ self._tags: dict[str, Any] = {}
19
+ self._content_tags: dict[str, Any] = {}
20
+ self._error: str | None = None
21
+
22
+ def set_tag(self, key: str, value: Any) -> None:
23
+ self._tags[key] = value
24
+
25
+ def set_content_tag(self, key: str, value: Any) -> None:
26
+ self._content_tags[key] = value
27
+
28
+ def set_error(self, error: str) -> None:
29
+ self._error = error
30
+
31
+ def end(self) -> None:
32
+ pass
33
+
34
+
35
+ class JsonFileTracer(Tracer):
36
+ """Tracer backed by InteractionLogger -- full backward compat.
37
+
38
+ The underlying ``InteractionLogger`` is exposed via the ``.logger``
39
+ property so that the web viewer and ``/log`` commands can continue
40
+ using it directly. All ``InteractionLogger`` public methods are
41
+ also proxied on this class so that duck-typed call sites (e.g.
42
+ ``_safe_log_request`` in *agent_loop*) work transparently.
43
+ """
44
+
45
+ def __init__(self, logger: InteractionLogger) -> None:
46
+ self._logger = logger
47
+ self._current_span: JsonFileSpan | None = None
48
+
49
+ @property
50
+ def logger(self) -> InteractionLogger:
51
+ """Expose the underlying logger for web_viewer / /log commands."""
52
+ return self._logger
53
+
54
+ @contextlib.contextmanager
55
+ def trace(
56
+ self,
57
+ operation_name: str,
58
+ tags: dict[str, Any] | None = None,
59
+ *,
60
+ parent_span: Span | None = None,
61
+ ) -> Iterator[Span]:
62
+ span = JsonFileSpan(operation_name)
63
+ if tags:
64
+ for k, v in tags.items():
65
+ span.set_tag(k, v)
66
+ prev = self._current_span
67
+ self._current_span = span
68
+ try:
69
+ yield span
70
+ except Exception as exc:
71
+ span.set_error(str(exc))
72
+ raise
73
+ finally:
74
+ span.end()
75
+ self._current_span = prev
76
+
77
+ def current_span(self) -> Span | None:
78
+ return self._current_span
79
+
80
+ # ---- Delegation: InteractionLogger methods for /log and web_viewer ----
81
+
82
+ @property
83
+ def session_id(self) -> str:
84
+ return self._logger.session_id
85
+
86
+ @session_id.setter
87
+ def session_id(self, value: str) -> None:
88
+ self._logger.session_id = value
89
+
90
+ def log_request(
91
+ self,
92
+ messages: list[dict[str, Any]],
93
+ tools: list[dict[str, Any]],
94
+ *,
95
+ provider_info: dict[str, Any] | None = None,
96
+ ) -> int:
97
+ return self._logger.log_request(messages, tools, provider_info=provider_info)
98
+
99
+ def log_response(self, seq: int, **kwargs: Any) -> None:
100
+ self._logger.log_response(seq, **kwargs)
101
+
102
+ def list_sessions(self) -> list[str]:
103
+ return self._logger.list_sessions()
104
+
105
+ def list_interactions(self, session_id: str) -> list[dict[str, Any]]:
106
+ return self._logger.list_interactions(session_id)
107
+
108
+ def get_interaction(self, session_id: str, seq: int) -> dict[str, Any]:
109
+ return self._logger.get_interaction(session_id, seq)
110
+
111
+ def subscribe_events(self, **kwargs: Any) -> Any:
112
+ return self._logger.subscribe_events(**kwargs)
113
+
114
+ def unsubscribe_events(self, event_queue: Any) -> None:
115
+ return self._logger.unsubscribe_events(event_queue)
@@ -0,0 +1,139 @@
1
+ """Langfuse tracer backend (optional dependency).
2
+
3
+ Requires the ``langfuse`` package::
4
+
5
+ pip install langfuse
6
+ # or: pip install bareagent-cli[langfuse]
7
+
8
+ Activated automatically when ``LANGFUSE_PUBLIC_KEY`` is set, or when
9
+ ``[tracing] langfuse = 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 LangfuseSpan(Span):
22
+ """Span backed by a Langfuse generation or span object."""
23
+
24
+ def __init__(self, langfuse_object: Any) -> None:
25
+ self._lf = langfuse_object
26
+ self._metadata: dict[str, Any] = {}
27
+ self._error: str | None = None
28
+
29
+ def set_tag(self, key: str, value: Any) -> None:
30
+ self._metadata[key] = value
31
+
32
+ def set_content_tag(self, key: str, value: Any) -> None:
33
+ if key == "input":
34
+ self._lf.input = value
35
+ elif key == "output":
36
+ self._lf.output = value
37
+ else:
38
+ self._metadata[key] = value
39
+
40
+ def set_error(self, error: str) -> None:
41
+ self._error = error
42
+
43
+ def end(self) -> None:
44
+ kwargs: dict[str, Any] = {}
45
+ if self._metadata:
46
+ kwargs["metadata"] = self._metadata
47
+ if self._error:
48
+ kwargs["level"] = "ERROR"
49
+ kwargs["status_message"] = self._error
50
+
51
+ # Langfuse generation objects accept usage on end()
52
+ input_tokens = self._metadata.get("input_tokens")
53
+ output_tokens = self._metadata.get("output_tokens")
54
+ if input_tokens is not None or output_tokens is not None:
55
+ kwargs["usage"] = {}
56
+ if input_tokens is not None:
57
+ kwargs["usage"]["input"] = int(input_tokens)
58
+ if output_tokens is not None:
59
+ kwargs["usage"]["output"] = int(output_tokens)
60
+
61
+ self._lf.end(**kwargs)
62
+
63
+
64
+ class LangfuseTracer(Tracer):
65
+ """Tracer that sends spans to Langfuse.
66
+
67
+ Reads credentials from standard Langfuse environment variables
68
+ (``LANGFUSE_PUBLIC_KEY``, ``LANGFUSE_SECRET_KEY``, ``LANGFUSE_HOST``).
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ *,
74
+ session_id: str = "default",
75
+ **langfuse_kwargs: Any,
76
+ ) -> None:
77
+ from langfuse import Langfuse # type: ignore
78
+
79
+ self._langfuse = Langfuse(**langfuse_kwargs)
80
+ self._session_id = session_id
81
+ self._trace = self._langfuse.trace(
82
+ name="bareagent-session",
83
+ session_id=session_id,
84
+ )
85
+ self._current_span: LangfuseSpan | None = None
86
+
87
+ @property
88
+ def session_id(self) -> str:
89
+ return self._session_id
90
+
91
+ @session_id.setter
92
+ def session_id(self, value: str) -> None:
93
+ self._session_id = value
94
+ self._trace = self._langfuse.trace(
95
+ name="bareagent-session",
96
+ session_id=value,
97
+ )
98
+
99
+ @contextlib.contextmanager
100
+ def trace(
101
+ self,
102
+ operation_name: str,
103
+ tags: dict[str, Any] | None = None,
104
+ *,
105
+ parent_span: Span | None = None,
106
+ ) -> Iterator[Span]:
107
+ parent = (
108
+ parent_span._lf # type: ignore[union-attr]
109
+ if isinstance(parent_span, LangfuseSpan)
110
+ else self._trace
111
+ )
112
+
113
+ if operation_name == "llm_call":
114
+ model = (tags or {}).get("model", "unknown")
115
+ lf_obj = parent.generation(name=operation_name, model=model, metadata=tags)
116
+ else:
117
+ lf_obj = parent.span(name=operation_name, metadata=tags)
118
+
119
+ span = LangfuseSpan(lf_obj)
120
+ prev = self._current_span
121
+ self._current_span = span
122
+ try:
123
+ yield span
124
+ except Exception as exc:
125
+ span.set_error(str(exc))
126
+ raise
127
+ finally:
128
+ span.end()
129
+ self._current_span = prev
130
+
131
+ def current_span(self) -> Span | None:
132
+ return self._current_span
133
+
134
+ def flush(self) -> None:
135
+ self._langfuse.flush()
136
+
137
+ def shutdown(self) -> None:
138
+ self._langfuse.flush()
139
+ self._langfuse.shutdown()