agent-trace-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.
- agent_trace/__init__.py +3 -0
- agent_trace/blame.py +471 -0
- agent_trace/blame_git.py +133 -0
- agent_trace/blame_meta.py +167 -0
- agent_trace/cli.py +2680 -0
- agent_trace/commit_link.py +226 -0
- agent_trace/config.py +137 -0
- agent_trace/context.py +385 -0
- agent_trace/conversations.py +358 -0
- agent_trace/git_notes.py +491 -0
- agent_trace/hooks/__init__.py +163 -0
- agent_trace/hooks/base.py +233 -0
- agent_trace/hooks/claude.py +483 -0
- agent_trace/hooks/codex.py +232 -0
- agent_trace/hooks/cursor.py +278 -0
- agent_trace/hooks/git.py +144 -0
- agent_trace/ledger.py +955 -0
- agent_trace/models.py +618 -0
- agent_trace/record.py +417 -0
- agent_trace/registry.py +230 -0
- agent_trace/remote.py +673 -0
- agent_trace/rewrite.py +176 -0
- agent_trace/rules.py +209 -0
- agent_trace/schemas/commit-link.schema.json +34 -0
- agent_trace/schemas/git-note.schema.json +88 -0
- agent_trace/schemas/ledger.schema.json +97 -0
- agent_trace/schemas/remotes.schema.json +30 -0
- agent_trace/schemas/sync-state.schema.json +49 -0
- agent_trace/schemas/trace-record.schema.json +130 -0
- agent_trace/session.py +136 -0
- agent_trace/storage.py +229 -0
- agent_trace/summary.py +465 -0
- agent_trace/summary_presets.py +188 -0
- agent_trace/sync.py +1182 -0
- agent_trace/telemetry.py +142 -0
- agent_trace/trace.py +460 -0
- agent_trace_cli-0.1.0.dist-info/METADATA +535 -0
- agent_trace_cli-0.1.0.dist-info/RECORD +42 -0
- agent_trace_cli-0.1.0.dist-info/WHEEL +5 -0
- agent_trace_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agent_trace_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- agent_trace_cli-0.1.0.dist-info/top_level.txt +1 -0
agent_trace/models.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typed models mirroring JSON Schemas in ``agent_trace/schemas/`` — trace records, ledgers,
|
|
3
|
+
commit links, git notes, remotes, and sync state.
|
|
4
|
+
|
|
5
|
+
Use :func:`from_dict` dispatchers and ``.to_dict()`` for JSONL round-trips.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, cast
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def schemas_dir() -> Path:
|
|
16
|
+
"""Directory containing ``*.schema.json`` shipped inside the ``agent_trace`` package."""
|
|
17
|
+
return Path(__file__).resolve().parent / "schemas"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --- Trace record (traces.jsonl) ---
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class LineHash:
|
|
25
|
+
line_offset: int
|
|
26
|
+
hash: str
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_dict(cls, d: dict[str, Any]) -> LineHash:
|
|
30
|
+
return cls(
|
|
31
|
+
line_offset=int(d["line_offset"]),
|
|
32
|
+
hash=str(d["hash"]),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict[str, Any]:
|
|
36
|
+
return {
|
|
37
|
+
"line_offset": self.line_offset,
|
|
38
|
+
"hash": self.hash,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Range:
|
|
44
|
+
start_line: int
|
|
45
|
+
end_line: int
|
|
46
|
+
content_hash: str | None = None
|
|
47
|
+
line_hashes: list[LineHash] | None = None
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_dict(cls, d: dict[str, Any]) -> Range:
|
|
51
|
+
lhs = d.get("line_hashes")
|
|
52
|
+
line_hashes: list[LineHash] | None = None
|
|
53
|
+
if isinstance(lhs, list):
|
|
54
|
+
line_hashes = [LineHash.from_dict(cast(dict[str, Any], x)) for x in lhs if isinstance(x, dict)]
|
|
55
|
+
return cls(
|
|
56
|
+
start_line=int(d["start_line"]),
|
|
57
|
+
end_line=int(d["end_line"]),
|
|
58
|
+
content_hash=d.get("content_hash"),
|
|
59
|
+
line_hashes=line_hashes,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
d: dict[str, Any] = {
|
|
64
|
+
"start_line": self.start_line,
|
|
65
|
+
"end_line": self.end_line,
|
|
66
|
+
}
|
|
67
|
+
if self.content_hash is not None:
|
|
68
|
+
d["content_hash"] = self.content_hash
|
|
69
|
+
if self.line_hashes is not None:
|
|
70
|
+
d["line_hashes"] = [lh.to_dict() for lh in self.line_hashes]
|
|
71
|
+
return d
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Contributor:
|
|
76
|
+
type: str
|
|
77
|
+
model_id: str | None = None
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_dict(cls, d: dict[str, Any]) -> Contributor:
|
|
81
|
+
return cls(type=str(d["type"]), model_id=d.get("model_id"))
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict[str, Any]:
|
|
84
|
+
out: dict[str, Any] = {"type": self.type}
|
|
85
|
+
if self.model_id is not None:
|
|
86
|
+
out["model_id"] = self.model_id
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class Conversation:
|
|
92
|
+
contributor: Contributor
|
|
93
|
+
ranges: list[Range]
|
|
94
|
+
id: str | None = None
|
|
95
|
+
content_sha256: str | None = None
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_dict(cls, d: dict[str, Any]) -> Conversation:
|
|
99
|
+
ranges = [Range.from_dict(cast(dict[str, Any], x)) for x in d.get("ranges", []) if isinstance(x, dict)]
|
|
100
|
+
return cls(
|
|
101
|
+
contributor=Contributor.from_dict(cast(dict[str, Any], d["contributor"])),
|
|
102
|
+
ranges=ranges,
|
|
103
|
+
id=d.get("id"),
|
|
104
|
+
content_sha256=d.get("content_sha256"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict[str, Any]:
|
|
108
|
+
out: dict[str, Any] = {
|
|
109
|
+
"contributor": self.contributor.to_dict(),
|
|
110
|
+
"ranges": [r.to_dict() for r in self.ranges],
|
|
111
|
+
}
|
|
112
|
+
if self.id is not None:
|
|
113
|
+
out["id"] = self.id
|
|
114
|
+
if self.content_sha256 is not None:
|
|
115
|
+
out["content_sha256"] = self.content_sha256
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class FileEntry:
|
|
121
|
+
path: str
|
|
122
|
+
conversations: list[Conversation]
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_dict(cls, d: dict[str, Any]) -> FileEntry:
|
|
126
|
+
convs = [
|
|
127
|
+
Conversation.from_dict(cast(dict[str, Any], x))
|
|
128
|
+
for x in d.get("conversations", [])
|
|
129
|
+
if isinstance(x, dict)
|
|
130
|
+
]
|
|
131
|
+
return cls(path=str(d["path"]), conversations=convs)
|
|
132
|
+
|
|
133
|
+
def to_dict(self) -> dict[str, Any]:
|
|
134
|
+
return {"path": self.path, "conversations": [c.to_dict() for c in self.conversations]}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class Trace:
|
|
139
|
+
"""One trace record (``traces.jsonl`` line)."""
|
|
140
|
+
|
|
141
|
+
version: str
|
|
142
|
+
id: str
|
|
143
|
+
timestamp: str
|
|
144
|
+
tool: dict[str, Any]
|
|
145
|
+
files: list[FileEntry]
|
|
146
|
+
vcs: dict[str, str] | None = None
|
|
147
|
+
metadata: dict[str, Any] | None = None
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_dict(cls, d: dict[str, Any]) -> Trace:
|
|
151
|
+
file_entries = [
|
|
152
|
+
FileEntry.from_dict(cast(dict[str, Any], x)) for x in d.get("files", []) if isinstance(x, dict)
|
|
153
|
+
]
|
|
154
|
+
vcs = d.get("vcs")
|
|
155
|
+
meta = d.get("metadata")
|
|
156
|
+
return cls(
|
|
157
|
+
version=str(d["version"]),
|
|
158
|
+
id=str(d["id"]),
|
|
159
|
+
timestamp=str(d["timestamp"]),
|
|
160
|
+
tool=dict(d["tool"]) if isinstance(d.get("tool"), dict) else {},
|
|
161
|
+
files=file_entries,
|
|
162
|
+
vcs=dict(vcs) if isinstance(vcs, dict) else None,
|
|
163
|
+
metadata=dict(meta) if isinstance(meta, dict) else None,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def to_dict(self) -> dict[str, Any]:
|
|
167
|
+
out: dict[str, Any] = {
|
|
168
|
+
"version": self.version,
|
|
169
|
+
"id": self.id,
|
|
170
|
+
"timestamp": self.timestamp,
|
|
171
|
+
"tool": self.tool,
|
|
172
|
+
"files": [f.to_dict() for f in self.files],
|
|
173
|
+
}
|
|
174
|
+
if self.vcs is not None:
|
|
175
|
+
out["vcs"] = self.vcs
|
|
176
|
+
if self.metadata is not None:
|
|
177
|
+
out["metadata"] = self.metadata
|
|
178
|
+
return out
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# --- Ledger (ledgers.jsonl) ---
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class LineEvidence:
|
|
186
|
+
"""Per-line audit trail: hash and content of each AI-attributed line."""
|
|
187
|
+
|
|
188
|
+
line: int
|
|
189
|
+
hash: str
|
|
190
|
+
content: str
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def from_dict(cls, d: dict[str, Any]) -> LineEvidence:
|
|
194
|
+
return cls(
|
|
195
|
+
line=int(d["line"]),
|
|
196
|
+
hash=str(d["hash"]),
|
|
197
|
+
content=str(d.get("content", "")),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def to_dict(self) -> dict[str, Any]:
|
|
201
|
+
return {"line": self.line, "hash": self.hash, "content": self.content}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class LineSegment:
|
|
206
|
+
"""An AI-attributed run of lines. Schema 2.0: only AI segments exist;
|
|
207
|
+
anything not in a segment is implicitly NO_ATTRIBUTION."""
|
|
208
|
+
|
|
209
|
+
start_line: int
|
|
210
|
+
end_line: int
|
|
211
|
+
trace_id: str
|
|
212
|
+
type: str = "ai"
|
|
213
|
+
model_id: str | None = None
|
|
214
|
+
conversation_id: str | None = None
|
|
215
|
+
evidence: list[LineEvidence] | None = None
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def from_dict(cls, d: dict[str, Any]) -> LineSegment:
|
|
219
|
+
ev_raw = d.get("evidence")
|
|
220
|
+
evidence: list[LineEvidence] | None = None
|
|
221
|
+
if isinstance(ev_raw, list):
|
|
222
|
+
evidence = [
|
|
223
|
+
LineEvidence.from_dict(cast(dict[str, Any], x))
|
|
224
|
+
for x in ev_raw
|
|
225
|
+
if isinstance(x, dict)
|
|
226
|
+
]
|
|
227
|
+
return cls(
|
|
228
|
+
start_line=int(d["start_line"]),
|
|
229
|
+
end_line=int(d["end_line"]),
|
|
230
|
+
type=str(d.get("type", "ai")),
|
|
231
|
+
trace_id=str(d["trace_id"]),
|
|
232
|
+
model_id=d.get("model_id"),
|
|
233
|
+
conversation_id=d.get("conversation_id"),
|
|
234
|
+
evidence=evidence,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def to_dict(self) -> dict[str, Any]:
|
|
238
|
+
out: dict[str, Any] = {
|
|
239
|
+
"start_line": self.start_line,
|
|
240
|
+
"end_line": self.end_line,
|
|
241
|
+
"type": self.type,
|
|
242
|
+
"trace_id": self.trace_id,
|
|
243
|
+
}
|
|
244
|
+
if self.model_id is not None:
|
|
245
|
+
out["model_id"] = self.model_id
|
|
246
|
+
if self.conversation_id is not None:
|
|
247
|
+
out["conversation_id"] = self.conversation_id
|
|
248
|
+
if self.evidence is not None:
|
|
249
|
+
out["evidence"] = [e.to_dict() for e in self.evidence]
|
|
250
|
+
return out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class FileLedger:
|
|
255
|
+
line_attributions: list[LineSegment]
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def from_dict(cls, d: dict[str, Any]) -> FileLedger:
|
|
259
|
+
segs = [
|
|
260
|
+
LineSegment.from_dict(cast(dict[str, Any], x))
|
|
261
|
+
for x in d.get("line_attributions", [])
|
|
262
|
+
if isinstance(x, dict)
|
|
263
|
+
]
|
|
264
|
+
return cls(line_attributions=segs)
|
|
265
|
+
|
|
266
|
+
def to_dict(self) -> dict[str, Any]:
|
|
267
|
+
return {"line_attributions": [s.to_dict() for s in self.line_attributions]}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class Ledger:
|
|
272
|
+
version: str
|
|
273
|
+
commit_sha: str
|
|
274
|
+
parent_sha: str | None
|
|
275
|
+
committed_at: str | None
|
|
276
|
+
created_at: str
|
|
277
|
+
trace_ids: list[str]
|
|
278
|
+
files: dict[str, FileLedger]
|
|
279
|
+
parent_committed_at: str | None = None
|
|
280
|
+
derived_from: dict[str, Any] | None = None
|
|
281
|
+
used_fallback: bool = False
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def from_dict(cls, d: dict[str, Any]) -> Ledger:
|
|
285
|
+
raw_files = d.get("files", {})
|
|
286
|
+
files: dict[str, FileLedger] = {}
|
|
287
|
+
if isinstance(raw_files, dict):
|
|
288
|
+
for path, fv in raw_files.items():
|
|
289
|
+
if isinstance(fv, dict):
|
|
290
|
+
files[str(path)] = FileLedger.from_dict(fv)
|
|
291
|
+
df = d.get("derived_from")
|
|
292
|
+
return cls(
|
|
293
|
+
version=str(d["version"]),
|
|
294
|
+
commit_sha=str(d["commit_sha"]),
|
|
295
|
+
parent_sha=d.get("parent_sha"),
|
|
296
|
+
parent_committed_at=d.get("parent_committed_at"),
|
|
297
|
+
committed_at=d.get("committed_at"),
|
|
298
|
+
created_at=str(d["created_at"]),
|
|
299
|
+
trace_ids=[str(x) for x in d.get("trace_ids", [])],
|
|
300
|
+
files=files,
|
|
301
|
+
derived_from=df if isinstance(df, dict) else None,
|
|
302
|
+
used_fallback=bool(d.get("used_fallback", False)),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def to_dict(self) -> dict[str, Any]:
|
|
306
|
+
out: dict[str, Any] = {
|
|
307
|
+
"version": self.version,
|
|
308
|
+
"commit_sha": self.commit_sha,
|
|
309
|
+
"parent_sha": self.parent_sha,
|
|
310
|
+
"committed_at": self.committed_at,
|
|
311
|
+
"created_at": self.created_at,
|
|
312
|
+
"trace_ids": list(self.trace_ids),
|
|
313
|
+
"files": {k: v.to_dict() for k, v in self.files.items()},
|
|
314
|
+
}
|
|
315
|
+
if self.parent_committed_at is not None:
|
|
316
|
+
out["parent_committed_at"] = self.parent_committed_at
|
|
317
|
+
if self.derived_from is not None:
|
|
318
|
+
out["derived_from"] = dict(self.derived_from)
|
|
319
|
+
if self.used_fallback:
|
|
320
|
+
out["used_fallback"] = True
|
|
321
|
+
return out
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# --- Commit link (commit-links.jsonl) ---
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclass
|
|
328
|
+
class CommitLink:
|
|
329
|
+
commit_sha: str
|
|
330
|
+
parent_sha: str | None
|
|
331
|
+
trace_ids: list[str]
|
|
332
|
+
files_changed: list[str]
|
|
333
|
+
committed_at: str | None
|
|
334
|
+
created_at: str
|
|
335
|
+
ledger: Ledger | None = None
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
def from_dict(cls, d: dict[str, Any]) -> CommitLink:
|
|
339
|
+
led = d.get("ledger")
|
|
340
|
+
ledger_obj: Ledger | None = None
|
|
341
|
+
if isinstance(led, dict):
|
|
342
|
+
ledger_obj = Ledger.from_dict(led)
|
|
343
|
+
return cls(
|
|
344
|
+
commit_sha=str(d["commit_sha"]),
|
|
345
|
+
parent_sha=d.get("parent_sha"),
|
|
346
|
+
trace_ids=[str(x) for x in d.get("trace_ids", [])],
|
|
347
|
+
files_changed=[str(x) for x in d.get("files_changed", [])],
|
|
348
|
+
committed_at=d.get("committed_at"),
|
|
349
|
+
created_at=str(d["created_at"]),
|
|
350
|
+
ledger=ledger_obj,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def to_dict(self) -> dict[str, Any]:
|
|
354
|
+
out: dict[str, Any] = {
|
|
355
|
+
"commit_sha": self.commit_sha,
|
|
356
|
+
"parent_sha": self.parent_sha,
|
|
357
|
+
"trace_ids": list(self.trace_ids),
|
|
358
|
+
"files_changed": list(self.files_changed),
|
|
359
|
+
"committed_at": self.committed_at,
|
|
360
|
+
"created_at": self.created_at,
|
|
361
|
+
}
|
|
362
|
+
if self.ledger is not None:
|
|
363
|
+
out["ledger"] = self.ledger.to_dict()
|
|
364
|
+
return out
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# --- Git note (refs/notes/agent-trace) ---
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@dataclass
|
|
371
|
+
class GitNoteStats:
|
|
372
|
+
"""Schema 2.0: only AI-line counts are tracked. Everything else is
|
|
373
|
+
implicitly NO_ATTRIBUTION. ``total_changed_lines`` is optional context."""
|
|
374
|
+
|
|
375
|
+
ai_lines: int
|
|
376
|
+
total_changed_lines: int | None = None
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
def from_dict(cls, d: dict[str, Any]) -> GitNoteStats:
|
|
380
|
+
total = d.get("total_changed_lines")
|
|
381
|
+
return cls(
|
|
382
|
+
ai_lines=int(d["ai_lines"]),
|
|
383
|
+
total_changed_lines=int(total) if total is not None else None,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def to_dict(self) -> dict[str, Any]:
|
|
387
|
+
out: dict[str, Any] = {"ai_lines": self.ai_lines}
|
|
388
|
+
if self.total_changed_lines is not None:
|
|
389
|
+
out["total_changed_lines"] = self.total_changed_lines
|
|
390
|
+
return out
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@dataclass
|
|
394
|
+
class GitNoteLedgerSection:
|
|
395
|
+
files: dict[str, FileLedger]
|
|
396
|
+
|
|
397
|
+
@classmethod
|
|
398
|
+
def from_dict(cls, d: dict[str, Any]) -> GitNoteLedgerSection:
|
|
399
|
+
raw = d.get("files", {})
|
|
400
|
+
files: dict[str, FileLedger] = {}
|
|
401
|
+
if isinstance(raw, dict):
|
|
402
|
+
for path, fv in raw.items():
|
|
403
|
+
if isinstance(fv, dict):
|
|
404
|
+
files[str(path)] = FileLedger.from_dict(fv)
|
|
405
|
+
return cls(files=files)
|
|
406
|
+
|
|
407
|
+
def to_dict(self) -> dict[str, Any]:
|
|
408
|
+
return {"files": {k: v.to_dict() for k, v in self.files.items()}}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@dataclass
|
|
412
|
+
class GitNote:
|
|
413
|
+
version: str
|
|
414
|
+
trace_ids: list[str]
|
|
415
|
+
ledger_hash: str
|
|
416
|
+
stats: GitNoteStats
|
|
417
|
+
ledger: GitNoteLedgerSection | None = None
|
|
418
|
+
summary: dict[str, str] | None = None
|
|
419
|
+
prompts: list[str] | None = None
|
|
420
|
+
all_session_conversations: list[dict[str, Any]] | None = None
|
|
421
|
+
|
|
422
|
+
@classmethod
|
|
423
|
+
def from_dict(cls, d: dict[str, Any]) -> GitNote:
|
|
424
|
+
ledger_sec: GitNoteLedgerSection | None = None
|
|
425
|
+
if isinstance(d.get("ledger"), dict):
|
|
426
|
+
ledger_sec = GitNoteLedgerSection.from_dict(cast(dict[str, Any], d["ledger"]))
|
|
427
|
+
summary = d.get("summary")
|
|
428
|
+
prompts = d.get("prompts")
|
|
429
|
+
pr: list[str] | None = None
|
|
430
|
+
if isinstance(prompts, list):
|
|
431
|
+
pr = [str(x) for x in prompts]
|
|
432
|
+
asc = d.get("all_session_conversations")
|
|
433
|
+
asc_list: list[dict[str, Any]] | None = None
|
|
434
|
+
if isinstance(asc, list):
|
|
435
|
+
asc_list = [cast(dict[str, Any], x) for x in asc if isinstance(x, dict)]
|
|
436
|
+
return cls(
|
|
437
|
+
version=str(d["version"]),
|
|
438
|
+
trace_ids=[str(x) for x in d.get("trace_ids", [])],
|
|
439
|
+
ledger_hash=str(d["ledger_hash"]),
|
|
440
|
+
stats=GitNoteStats.from_dict(cast(dict[str, Any], d["stats"])),
|
|
441
|
+
ledger=ledger_sec,
|
|
442
|
+
summary=dict(summary) if isinstance(summary, dict) else None,
|
|
443
|
+
prompts=pr,
|
|
444
|
+
all_session_conversations=asc_list,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def to_dict(self) -> dict[str, Any]:
|
|
448
|
+
out: dict[str, Any] = {
|
|
449
|
+
"version": self.version,
|
|
450
|
+
"trace_ids": list(self.trace_ids),
|
|
451
|
+
"ledger_hash": self.ledger_hash,
|
|
452
|
+
"stats": self.stats.to_dict(),
|
|
453
|
+
}
|
|
454
|
+
if self.ledger is not None:
|
|
455
|
+
out["ledger"] = self.ledger.to_dict()
|
|
456
|
+
if self.summary is not None:
|
|
457
|
+
out["summary"] = dict(self.summary)
|
|
458
|
+
if self.prompts is not None:
|
|
459
|
+
out["prompts"] = list(self.prompts)
|
|
460
|
+
if self.all_session_conversations is not None:
|
|
461
|
+
out["all_session_conversations"] = [dict(x) for x in self.all_session_conversations]
|
|
462
|
+
return out
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# --- Remotes (~/.agent-trace/projects/<id>/remotes.json) ---
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@dataclass
|
|
469
|
+
class AuthConfig:
|
|
470
|
+
type: str
|
|
471
|
+
token_ref: str
|
|
472
|
+
|
|
473
|
+
@classmethod
|
|
474
|
+
def from_dict(cls, d: dict[str, Any]) -> AuthConfig:
|
|
475
|
+
return cls(type=str(d["type"]), token_ref=str(d["token_ref"]))
|
|
476
|
+
|
|
477
|
+
def to_dict(self) -> dict[str, Any]:
|
|
478
|
+
return {"type": self.type, "token_ref": self.token_ref}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@dataclass
|
|
482
|
+
class Remote:
|
|
483
|
+
url: str
|
|
484
|
+
auth: AuthConfig | None = None
|
|
485
|
+
|
|
486
|
+
@classmethod
|
|
487
|
+
def from_dict(cls, d: dict[str, Any]) -> Remote:
|
|
488
|
+
auth = d.get("auth")
|
|
489
|
+
auth_obj: AuthConfig | None = None
|
|
490
|
+
if isinstance(auth, dict):
|
|
491
|
+
auth_obj = AuthConfig.from_dict(auth)
|
|
492
|
+
return cls(url=str(d["url"]), auth=auth_obj)
|
|
493
|
+
|
|
494
|
+
def to_dict(self) -> dict[str, Any]:
|
|
495
|
+
out: dict[str, Any] = {"url": self.url}
|
|
496
|
+
if self.auth is not None:
|
|
497
|
+
out["auth"] = self.auth.to_dict()
|
|
498
|
+
return out
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@dataclass
|
|
502
|
+
class RemotesConfig:
|
|
503
|
+
"""Root object of ``remotes.json``: remote name → config."""
|
|
504
|
+
|
|
505
|
+
remotes: dict[str, Remote] = field(default_factory=dict)
|
|
506
|
+
|
|
507
|
+
@classmethod
|
|
508
|
+
def from_dict(cls, d: dict[str, Any]) -> RemotesConfig:
|
|
509
|
+
remotes: dict[str, Remote] = {}
|
|
510
|
+
for name, rv in d.items():
|
|
511
|
+
if isinstance(rv, dict):
|
|
512
|
+
remotes[str(name)] = Remote.from_dict(rv)
|
|
513
|
+
return cls(remotes=remotes)
|
|
514
|
+
|
|
515
|
+
def to_dict(self) -> dict[str, Any]:
|
|
516
|
+
return {k: v.to_dict() for k, v in self.remotes.items()}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# --- Sync state (~/.agent-trace/projects/<id>/sync-state.json) ---
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@dataclass
|
|
523
|
+
class SyncedManifest:
|
|
524
|
+
trace_ids: list[str] = field(default_factory=list)
|
|
525
|
+
ledger_shas: list[str] = field(default_factory=list)
|
|
526
|
+
commit_link_shas: list[str] = field(default_factory=list)
|
|
527
|
+
blob_shas: list[str] = field(default_factory=list)
|
|
528
|
+
conversation_ids: list[str] = field(default_factory=list)
|
|
529
|
+
summary_keys: list[str] = field(default_factory=list)
|
|
530
|
+
|
|
531
|
+
@classmethod
|
|
532
|
+
def from_dict(cls, d: dict[str, Any]) -> SyncedManifest:
|
|
533
|
+
def _strs(key: str) -> list[str]:
|
|
534
|
+
v = d.get(key)
|
|
535
|
+
return [str(x) for x in v if isinstance(x, str)] if isinstance(v, list) else []
|
|
536
|
+
|
|
537
|
+
return cls(
|
|
538
|
+
trace_ids=_strs("trace_ids"),
|
|
539
|
+
ledger_shas=_strs("ledger_shas"),
|
|
540
|
+
commit_link_shas=_strs("commit_link_shas"),
|
|
541
|
+
blob_shas=_strs("blob_shas"),
|
|
542
|
+
conversation_ids=_strs("conversation_ids"),
|
|
543
|
+
summary_keys=_strs("summary_keys"),
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
def to_dict(self) -> dict[str, Any]:
|
|
547
|
+
return asdict(self)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@dataclass
|
|
551
|
+
class PullCursors:
|
|
552
|
+
traces: str | None = None
|
|
553
|
+
ledgers: str | None = None
|
|
554
|
+
commit_links: str | None = None
|
|
555
|
+
conversations: str | None = None
|
|
556
|
+
summaries: str | None = None
|
|
557
|
+
|
|
558
|
+
@classmethod
|
|
559
|
+
def from_dict(cls, d: dict[str, Any]) -> PullCursors:
|
|
560
|
+
return cls(
|
|
561
|
+
traces=d.get("traces"),
|
|
562
|
+
ledgers=d.get("ledgers"),
|
|
563
|
+
commit_links=d.get("commit_links"),
|
|
564
|
+
conversations=d.get("conversations"),
|
|
565
|
+
summaries=d.get("summaries"),
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
def to_dict(self) -> dict[str, Any]:
|
|
569
|
+
return asdict(self)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@dataclass
|
|
573
|
+
class RemoteSyncState:
|
|
574
|
+
synced: SyncedManifest = field(default_factory=SyncedManifest)
|
|
575
|
+
cursor: PullCursors = field(default_factory=PullCursors)
|
|
576
|
+
|
|
577
|
+
@classmethod
|
|
578
|
+
def from_dict(cls, d: dict[str, Any]) -> RemoteSyncState:
|
|
579
|
+
synced = d.get("synced")
|
|
580
|
+
cursor = d.get("cursor")
|
|
581
|
+
return cls(
|
|
582
|
+
synced=SyncedManifest.from_dict(cast(dict[str, Any], synced)) if isinstance(synced, dict) else SyncedManifest(),
|
|
583
|
+
cursor=PullCursors.from_dict(cast(dict[str, Any], cursor)) if isinstance(cursor, dict) else PullCursors(),
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
def to_dict(self) -> dict[str, Any]:
|
|
587
|
+
return {"synced": self.synced.to_dict(), "cursor": self.cursor.to_dict()}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@dataclass
|
|
591
|
+
class SyncState:
|
|
592
|
+
remotes: dict[str, RemoteSyncState]
|
|
593
|
+
version: int = 2
|
|
594
|
+
|
|
595
|
+
@classmethod
|
|
596
|
+
def from_dict(cls, d: dict[str, Any]) -> SyncState:
|
|
597
|
+
raw = d.get("remotes", {})
|
|
598
|
+
remotes: dict[str, RemoteSyncState] = {}
|
|
599
|
+
if isinstance(raw, dict):
|
|
600
|
+
for name, rv in raw.items():
|
|
601
|
+
if isinstance(rv, dict):
|
|
602
|
+
remotes[str(name)] = RemoteSyncState.from_dict(rv)
|
|
603
|
+
version = d.get("version") if isinstance(d.get("version"), int) else 2
|
|
604
|
+
return cls(remotes=remotes, version=version)
|
|
605
|
+
|
|
606
|
+
def to_dict(self) -> dict[str, Any]:
|
|
607
|
+
return {
|
|
608
|
+
"version": self.version,
|
|
609
|
+
"remotes": {k: v.to_dict() for k, v in self.remotes.items()},
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def trace_from_dict(d: dict[str, Any]) -> Trace:
|
|
614
|
+
return Trace.from_dict(d)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def ledger_from_dict(d: dict[str, Any]) -> Ledger:
|
|
618
|
+
return Ledger.from_dict(d)
|