ai-agent-tap 0.3.3__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.
- agent_tap/__init__.py +3 -0
- agent_tap/__main__.py +5 -0
- agent_tap/adapters/__init__.py +0 -0
- agent_tap/adapters/base.py +120 -0
- agent_tap/adapters/claude_code.py +312 -0
- agent_tap/adapters/codex.py +362 -0
- agent_tap/adapters/copilot_cli.py +388 -0
- agent_tap/adapters/declarative.py +263 -0
- agent_tap/adapters/hermes.py +575 -0
- agent_tap/adapters/omniroute.py +349 -0
- agent_tap/adapters/raven.py +836 -0
- agent_tap/adapters/service.py +599 -0
- agent_tap/bugreport.py +354 -0
- agent_tap/cli.py +354 -0
- agent_tap/core/__init__.py +0 -0
- agent_tap/core/config.py +96 -0
- agent_tap/core/paths.py +95 -0
- agent_tap/core/schema.py +73 -0
- agent_tap/dashboard/__init__.py +757 -0
- agent_tap/dashboard/static/index.html +926 -0
- agent_tap/digest.py +352 -0
- agent_tap/emitter.py +378 -0
- agent_tap/examples/aider.toml +16 -0
- agent_tap/examples/cline.toml +22 -0
- agent_tap/examples/services/everos.toml +6 -0
- agent_tap/examples/services/firecrawl.toml +13 -0
- agent_tap/examples/services/freellmapi.toml +6 -0
- agent_tap/examples/services/generic-docker.toml +7 -0
- agent_tap/examples/services/generic-http.toml +8 -0
- agent_tap/examples/services/searxng.toml +6 -0
- agent_tap/onboard.py +102 -0
- agent_tap/onboarding.py +193 -0
- agent_tap/platform/__init__.py +0 -0
- agent_tap/platform/scheduler.py +846 -0
- agent_tap/platform/service.py +141 -0
- agent_tap/redact.py +46 -0
- agent_tap/state.py +288 -0
- ai_agent_tap-0.3.3.dist-info/METADATA +298 -0
- ai_agent_tap-0.3.3.dist-info/RECORD +43 -0
- ai_agent_tap-0.3.3.dist-info/WHEEL +5 -0
- ai_agent_tap-0.3.3.dist-info/entry_points.txt +10 -0
- ai_agent_tap-0.3.3.dist-info/licenses/LICENSE +21 -0
- ai_agent_tap-0.3.3.dist-info/top_level.txt +1 -0
agent_tap/__init__.py
ADDED
agent_tap/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Adapter contract and discovery (entry-point group ``agent_tap.harnesses`` + declarative TOML adapters)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from importlib.metadata import entry_points
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from ..core.config import Config
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..emitter import SpanStore
|
|
14
|
+
from ..state import StateStore
|
|
15
|
+
|
|
16
|
+
ENTRY_POINT_GROUP = "agent_tap.harnesses"
|
|
17
|
+
|
|
18
|
+
# stage name -> (spans_emitted, errors_emitted)
|
|
19
|
+
PollResult = dict[str, tuple[int, int]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Context:
|
|
24
|
+
config: Config
|
|
25
|
+
|
|
26
|
+
def dir_for(self, name: str, default: Path | None = None) -> Path | None:
|
|
27
|
+
"""Config/env override for an adapter's data dir, else the OS-discovered default."""
|
|
28
|
+
return self.config.harness_dir(name) or default
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class HarnessAdapter(Protocol):
|
|
33
|
+
name: str
|
|
34
|
+
kind: str # "harness" (default when missing), "gateway" or "service"
|
|
35
|
+
|
|
36
|
+
def discover(self, ctx: Context) -> bool:
|
|
37
|
+
"""True when this harness has data on this machine."""
|
|
38
|
+
|
|
39
|
+
def poll(self, ctx: Context, state: "StateStore", store: "SpanStore", cutoff_ts: float) -> PollResult:
|
|
40
|
+
"""Emit spans newer than cutoff_ts into store; return per-stage (spans, errors)."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def builtin_adapters() -> dict[str, Callable[[], HarnessAdapter]]:
|
|
44
|
+
"""Fallback registry so a source checkout works before entry points are (re)installed."""
|
|
45
|
+
from .claude_code import ClaudeCodeAdapter
|
|
46
|
+
from .codex import CodexAdapter
|
|
47
|
+
from .copilot_cli import CopilotCliAdapter
|
|
48
|
+
from .hermes import HermesAdapter
|
|
49
|
+
from .omniroute import OmniRouteAdapter
|
|
50
|
+
from .raven import RavenAdapter
|
|
51
|
+
|
|
52
|
+
from .service import BUILTIN_SERVICE_ADAPTERS
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
**BUILTIN_SERVICE_ADAPTERS,
|
|
56
|
+
"raven": RavenAdapter, "hermes": HermesAdapter, "claude-code": ClaudeCodeAdapter,
|
|
57
|
+
"codex": CodexAdapter, "copilot-cli": CopilotCliAdapter, "omniroute": OmniRouteAdapter,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def adapter_kind(adapter: Any) -> str:
|
|
62
|
+
from ..core.schema import SERVICE_KINDS, service_kind_for
|
|
63
|
+
|
|
64
|
+
kind = getattr(adapter, "kind", None)
|
|
65
|
+
return kind if kind in SERVICE_KINDS else service_kind_for(adapter.name)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def merge_service_adapters(adapters: list, extra: list) -> list:
|
|
69
|
+
"""Add service adapters; a configured service replaces an adapter with the same name."""
|
|
70
|
+
names = {a.name for a in extra}
|
|
71
|
+
return [a for a in adapters if a.name not in names] + list(extra)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_adapters(config: Config) -> list[HarnessAdapter]:
|
|
75
|
+
"""Built-ins + third-party entry points + declarative ``*.toml`` in config.adapters_dir."""
|
|
76
|
+
factories: dict[str, Callable[[], HarnessAdapter]] = dict(builtin_adapters())
|
|
77
|
+
for ep in entry_points(group=ENTRY_POINT_GROUP):
|
|
78
|
+
try:
|
|
79
|
+
factories[ep.name] = ep.load()
|
|
80
|
+
except Exception:
|
|
81
|
+
continue
|
|
82
|
+
adapters = [f() for f in factories.values()]
|
|
83
|
+
from .declarative import load_declarative_adapters
|
|
84
|
+
from .service import load_service_adapters
|
|
85
|
+
|
|
86
|
+
adapters += load_declarative_adapters(config.adapters_dir)
|
|
87
|
+
adapters = merge_service_adapters(adapters, load_service_adapters(config))
|
|
88
|
+
if config.enabled_services is not None:
|
|
89
|
+
adapters = [a for a in adapters if a.name in config.enabled_services]
|
|
90
|
+
return adapters
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run_adapters(
|
|
94
|
+
adapters: list[HarnessAdapter], ctx: Context, state: "StateStore", store: "SpanStore", cutoff_ts: float
|
|
95
|
+
) -> dict[str, dict[str, Any]]:
|
|
96
|
+
"""Poll every discovered adapter, isolating failures and recording per-stage source health."""
|
|
97
|
+
results: dict[str, dict[str, Any]] = {}
|
|
98
|
+
for ad in adapters:
|
|
99
|
+
try:
|
|
100
|
+
if not ad.discover(ctx):
|
|
101
|
+
continue
|
|
102
|
+
stages = ad.poll(ctx, state, store, cutoff_ts)
|
|
103
|
+
store.flush()
|
|
104
|
+
for stage, (sp, err) in stages.items():
|
|
105
|
+
state.record_source_health(
|
|
106
|
+
stage, spans_emitted=sp, errors_emitted=err,
|
|
107
|
+
last_event_at=time.time() if sp > 0 else None, last_error=None,
|
|
108
|
+
)
|
|
109
|
+
results[stage] = {"spans": sp, "errors": err}
|
|
110
|
+
except Exception as e: # one broken adapter must not stop the others
|
|
111
|
+
try:
|
|
112
|
+
state.record_source_health(ad.name, last_error=str(e))
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
results[ad.name] = {"spans": 0, "errors": 0, "exception": str(e)}
|
|
116
|
+
try:
|
|
117
|
+
state.commit()
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
return results
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sqlite3
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from ..core import paths
|
|
13
|
+
from ..emitter import SpanStore as PhoenixEmitter, build_span, ensure_utc
|
|
14
|
+
from ..state import SessionLink, StateStore, make_span_id, make_trace_id
|
|
15
|
+
from .base import Context, PollResult
|
|
16
|
+
|
|
17
|
+
_ROOTS: list[Path] = []
|
|
18
|
+
def poll_claude(
|
|
19
|
+
store: StateStore,
|
|
20
|
+
emitter: PhoenixEmitter,
|
|
21
|
+
cutoff_ts: float,
|
|
22
|
+
) -> tuple[int, int]:
|
|
23
|
+
spans_count = 0
|
|
24
|
+
errors_count = 0
|
|
25
|
+
if not _ROOTS:
|
|
26
|
+
return 0, 0
|
|
27
|
+
|
|
28
|
+
for cf in [f for r in _ROOTS for f in glob.glob(str(r / "**" / "*.jsonl"), recursive=True)]:
|
|
29
|
+
try:
|
|
30
|
+
st = os.stat(cf)
|
|
31
|
+
except OSError:
|
|
32
|
+
continue
|
|
33
|
+
if st.st_mtime < cutoff_ts:
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
offset, prev_mtime_ns, prev_ctime = store.get_file_watermark(cf)
|
|
37
|
+
if st.st_size < offset or (prev_ctime and int(st.st_ctime) != prev_ctime):
|
|
38
|
+
offset = 0
|
|
39
|
+
if st.st_size == offset and st.st_mtime_ns == prev_mtime_ns:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
p = Path(cf)
|
|
43
|
+
is_subagent = p.parent.name == "subagents"
|
|
44
|
+
file_stem = p.stem
|
|
45
|
+
|
|
46
|
+
# Inspect header lines to determine sessionId / parentSessionId
|
|
47
|
+
parent_claude_sid: Optional[str] = None
|
|
48
|
+
claude_sid = file_stem
|
|
49
|
+
cwd = ""
|
|
50
|
+
first_ts = st.st_ctime
|
|
51
|
+
try:
|
|
52
|
+
with open(cf, "r", encoding="utf-8", errors="replace") as f:
|
|
53
|
+
for _ in range(5):
|
|
54
|
+
l0 = f.readline()
|
|
55
|
+
if not l0:
|
|
56
|
+
break
|
|
57
|
+
d0 = json.loads(l0)
|
|
58
|
+
if d0.get("type") == "fork-context-ref":
|
|
59
|
+
parent_claude_sid = d0.get("parentSessionId")
|
|
60
|
+
claude_sid = f"{parent_claude_sid}:subagent:{d0.get('agentId') or file_stem}"
|
|
61
|
+
if d0.get("sessionId") and not is_subagent:
|
|
62
|
+
claude_sid = d0["sessionId"]
|
|
63
|
+
if d0.get("cwd") and not cwd:
|
|
64
|
+
cwd = d0["cwd"]
|
|
65
|
+
if d0.get("timestamp") and first_ts == st.st_ctime:
|
|
66
|
+
first_ts = d0["timestamp"]
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
lookup_sid = parent_claude_sid if (is_subagent and parent_claude_sid) else claude_sid
|
|
71
|
+
st_dt = ensure_utc(first_ts)
|
|
72
|
+
link = store.resolve_session_link("claude", lookup_sid, int(st_dt.timestamp() * 1000))
|
|
73
|
+
|
|
74
|
+
if link:
|
|
75
|
+
trace_id = link.trace_id
|
|
76
|
+
top_sess_id = link.raven_session_key
|
|
77
|
+
if is_subagent and parent_claude_sid:
|
|
78
|
+
parent_span_id = make_span_id("claude", "session", parent_claude_sid)
|
|
79
|
+
conf = "exact_2nd_degree"
|
|
80
|
+
else:
|
|
81
|
+
parent_span_id = link.raven_node_span_id
|
|
82
|
+
conf = link.confidence
|
|
83
|
+
else:
|
|
84
|
+
root_sid = parent_claude_sid if (is_subagent and parent_claude_sid) else claude_sid
|
|
85
|
+
trace_id = make_trace_id("claude", root_sid)
|
|
86
|
+
top_sess_id = root_sid
|
|
87
|
+
parent_span_id = make_span_id("claude", "session", parent_claude_sid) if (is_subagent and parent_claude_sid) else None
|
|
88
|
+
conf = "exact_claude_subagent" if is_subagent else "standalone"
|
|
89
|
+
|
|
90
|
+
sess_span_id = make_span_id("claude", "session", claude_sid)
|
|
91
|
+
if offset == 0:
|
|
92
|
+
emitter.add(
|
|
93
|
+
build_span(
|
|
94
|
+
trace_id=trace_id,
|
|
95
|
+
span_id=sess_span_id,
|
|
96
|
+
name=f"claude.{'subagent' if is_subagent else 'session'} ({file_stem[:18]})",
|
|
97
|
+
span_kind="AGENT",
|
|
98
|
+
parent_id=parent_span_id,
|
|
99
|
+
start_time=first_ts,
|
|
100
|
+
end_time=st.st_mtime,
|
|
101
|
+
status_code="OK",
|
|
102
|
+
attributes={
|
|
103
|
+
"session.id": top_sess_id,
|
|
104
|
+
"child.session_id": claude_sid,
|
|
105
|
+
"harness": "claude",
|
|
106
|
+
"agent.name": "Claude Code Subagent" if is_subagent else "Claude Code",
|
|
107
|
+
"working_directory": cwd,
|
|
108
|
+
"correlation.confidence": conf,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
spans_count += 1
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
with open(cf, "rb") as bf:
|
|
116
|
+
bf.seek(offset)
|
|
117
|
+
raw = bf.read()
|
|
118
|
+
new_offset = bf.tell()
|
|
119
|
+
except OSError:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
for idx, line in enumerate(raw.decode("utf-8", errors="replace").splitlines()):
|
|
123
|
+
if not line.strip():
|
|
124
|
+
continue
|
|
125
|
+
try:
|
|
126
|
+
rec = json.loads(line)
|
|
127
|
+
except Exception:
|
|
128
|
+
continue
|
|
129
|
+
rtype = rec.get("type")
|
|
130
|
+
ts = rec.get("timestamp") or st.st_mtime
|
|
131
|
+
uuid = rec.get("uuid") or f"{offset}_{idx}"
|
|
132
|
+
msg = rec.get("message") or {}
|
|
133
|
+
content_list = msg.get("content") if isinstance(msg, dict) else None
|
|
134
|
+
|
|
135
|
+
if rtype == "user":
|
|
136
|
+
if isinstance(content_list, list):
|
|
137
|
+
for c_idx, item in enumerate(content_list):
|
|
138
|
+
if not isinstance(item, dict):
|
|
139
|
+
continue
|
|
140
|
+
if item.get("type") == "tool_result":
|
|
141
|
+
is_err = bool(item.get("is_error"))
|
|
142
|
+
t_out = str(item.get("content") or "")
|
|
143
|
+
if is_err:
|
|
144
|
+
errors_count += 1
|
|
145
|
+
err_cls = None
|
|
146
|
+
if is_err:
|
|
147
|
+
err_cls = "claude_sleep_guardrail_blocked" if "Blocked: sleep" in t_out else "claude_tool_error"
|
|
148
|
+
emitter.add(
|
|
149
|
+
build_span(
|
|
150
|
+
trace_id=trace_id,
|
|
151
|
+
span_id=make_span_id("claude", "tool_res", claude_sid, uuid, c_idx),
|
|
152
|
+
name="claude.tool_result",
|
|
153
|
+
span_kind="TOOL",
|
|
154
|
+
parent_id=sess_span_id,
|
|
155
|
+
start_time=ts,
|
|
156
|
+
end_time=ts,
|
|
157
|
+
status_code="ERROR" if is_err else "OK",
|
|
158
|
+
status_message=t_out[:500] if is_err else "",
|
|
159
|
+
attributes={
|
|
160
|
+
"session.id": top_sess_id,
|
|
161
|
+
"child.session_id": claude_sid,
|
|
162
|
+
"harness": "claude",
|
|
163
|
+
"tool.id": item.get("tool_use_id") or "",
|
|
164
|
+
"output.value": t_out[:12000],
|
|
165
|
+
"error.class": err_cls,
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
spans_count += 1
|
|
170
|
+
elif item.get("type") == "text":
|
|
171
|
+
emitter.add(
|
|
172
|
+
build_span(
|
|
173
|
+
trace_id=trace_id,
|
|
174
|
+
span_id=make_span_id("claude", "user", claude_sid, uuid, c_idx),
|
|
175
|
+
name="claude.turn.user",
|
|
176
|
+
span_kind="CHAIN",
|
|
177
|
+
parent_id=sess_span_id,
|
|
178
|
+
start_time=ts,
|
|
179
|
+
end_time=ts,
|
|
180
|
+
status_code="OK",
|
|
181
|
+
attributes={
|
|
182
|
+
"session.id": top_sess_id,
|
|
183
|
+
"child.session_id": claude_sid,
|
|
184
|
+
"harness": "claude",
|
|
185
|
+
"input.value": str(item.get("text") or "")[:16000],
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
spans_count += 1
|
|
190
|
+
elif isinstance(msg.get("content"), str):
|
|
191
|
+
emitter.add(
|
|
192
|
+
build_span(
|
|
193
|
+
trace_id=trace_id,
|
|
194
|
+
span_id=make_span_id("claude", "user", claude_sid, uuid),
|
|
195
|
+
name="claude.turn.user",
|
|
196
|
+
span_kind="CHAIN",
|
|
197
|
+
parent_id=sess_span_id,
|
|
198
|
+
start_time=ts,
|
|
199
|
+
end_time=ts,
|
|
200
|
+
status_code="OK",
|
|
201
|
+
attributes={
|
|
202
|
+
"session.id": top_sess_id,
|
|
203
|
+
"child.session_id": claude_sid,
|
|
204
|
+
"harness": "claude",
|
|
205
|
+
"input.value": msg["content"][:16000],
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
spans_count += 1
|
|
210
|
+
|
|
211
|
+
elif rtype == "assistant" and isinstance(msg, dict):
|
|
212
|
+
usage = msg.get("usage") or {}
|
|
213
|
+
in_tok = (
|
|
214
|
+
int(usage.get("input_tokens") or 0)
|
|
215
|
+
+ int(usage.get("cache_creation_input_tokens") or 0)
|
|
216
|
+
+ int(usage.get("cache_read_input_tokens") or 0)
|
|
217
|
+
)
|
|
218
|
+
out_tok = int(usage.get("output_tokens") or 0)
|
|
219
|
+
model_nm = rec.get("requestedModel") or msg.get("model") or ""
|
|
220
|
+
texts = []
|
|
221
|
+
tool_uses = []
|
|
222
|
+
if isinstance(content_list, list):
|
|
223
|
+
for item in content_list:
|
|
224
|
+
if isinstance(item, dict):
|
|
225
|
+
if item.get("type") == "text":
|
|
226
|
+
texts.append(str(item.get("text") or ""))
|
|
227
|
+
elif item.get("type") == "tool_use":
|
|
228
|
+
tool_uses.append(item)
|
|
229
|
+
|
|
230
|
+
asst_span_id = make_span_id("claude", "asst", claude_sid, uuid)
|
|
231
|
+
emitter.add(
|
|
232
|
+
build_span(
|
|
233
|
+
trace_id=trace_id,
|
|
234
|
+
span_id=asst_span_id,
|
|
235
|
+
name="claude.turn.assistant",
|
|
236
|
+
span_kind="LLM",
|
|
237
|
+
parent_id=sess_span_id,
|
|
238
|
+
start_time=ts,
|
|
239
|
+
end_time=ts,
|
|
240
|
+
status_code="OK",
|
|
241
|
+
attributes={
|
|
242
|
+
"session.id": top_sess_id,
|
|
243
|
+
"child.session_id": claude_sid,
|
|
244
|
+
"harness": "claude",
|
|
245
|
+
"llm.model_name": model_nm,
|
|
246
|
+
"llm.token_count.prompt": in_tok,
|
|
247
|
+
"llm.token_count.completion": out_tok,
|
|
248
|
+
"llm.token_count.total": in_tok + out_tok,
|
|
249
|
+
"output.value": "\n".join(texts)[:16000],
|
|
250
|
+
"tool.call_count": len(tool_uses),
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
spans_count += 1
|
|
255
|
+
|
|
256
|
+
for tu in tool_uses:
|
|
257
|
+
tname = tu.get("name") or "tool"
|
|
258
|
+
tid = tu.get("id") or uuid
|
|
259
|
+
tinput = tu.get("input") or {}
|
|
260
|
+
is_skill = tname in ("Skill", "read_skill", "use_skill")
|
|
261
|
+
emitter.add(
|
|
262
|
+
build_span(
|
|
263
|
+
trace_id=trace_id,
|
|
264
|
+
span_id=make_span_id("claude", "tool_use", claude_sid, tid),
|
|
265
|
+
name=f"claude.{'skill' if is_skill else 'tool'}.{tname}",
|
|
266
|
+
span_kind="TOOL",
|
|
267
|
+
parent_id=asst_span_id,
|
|
268
|
+
start_time=ts,
|
|
269
|
+
end_time=ts,
|
|
270
|
+
status_code="OK",
|
|
271
|
+
attributes={
|
|
272
|
+
"session.id": top_sess_id,
|
|
273
|
+
"child.session_id": claude_sid,
|
|
274
|
+
"harness": "claude",
|
|
275
|
+
"tool.name": tname,
|
|
276
|
+
"tool.id": tid,
|
|
277
|
+
"is_skill": is_skill,
|
|
278
|
+
"input.value": json.dumps(tinput, ensure_ascii=False)[:8000],
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
spans_count += 1
|
|
283
|
+
|
|
284
|
+
store.set_file_watermark("claude", cf, new_offset, st.st_mtime_ns, int(st.st_ctime))
|
|
285
|
+
|
|
286
|
+
return spans_count, errors_count
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ============================================================================
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class ClaudeCodeAdapter:
|
|
293
|
+
name = "claude-code"
|
|
294
|
+
|
|
295
|
+
def discover(self, ctx: Context) -> bool:
|
|
296
|
+
return bool(self._roots(ctx))
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def _roots(ctx: Context) -> list[Path]:
|
|
300
|
+
o = ctx.dir_for("claude-code")
|
|
301
|
+
return [o] if o else paths.claude_roots()
|
|
302
|
+
|
|
303
|
+
def poll(self, ctx: Context, state: StateStore, store: PhoenixEmitter, cutoff_ts: float) -> PollResult:
|
|
304
|
+
global _ROOTS
|
|
305
|
+
_ROOTS = self._roots(ctx)
|
|
306
|
+
out: PollResult = {}
|
|
307
|
+
try:
|
|
308
|
+
out["claude"] = poll_claude(state, store, cutoff_ts)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
state.record_source_health("claude", last_error=str(e))
|
|
311
|
+
out["claude"] = (0, 0)
|
|
312
|
+
return out
|