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,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()
|