Rohan03 commited on
Commit
7498b52
·
verified ·
1 Parent(s): f0bf034

V2 merge: purpose_agent/trace.py

Browse files
Files changed (1) hide show
  1. purpose_agent/trace.py +139 -0
purpose_agent/trace.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ trace.py — Structured execution traces with JSONL persistence.
3
+
4
+ Every Orchestrator step emits TraceEvents. A Trace is the full sequence
5
+ for one task. Traces are the raw material for memory extraction, debugging,
6
+ and evaluation. They are append-only and immutable once written.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import time
13
+ import uuid
14
+ from dataclasses import dataclass, field, asdict
15
+ from pathlib import Path
16
+ from typing import Any, Iterator
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class TraceEvent:
23
+ """A single event in an execution trace."""
24
+ kind: str # "action", "score", "tool_call", "tool_result", "error", "memory_read", "memory_write"
25
+ step: int = 0
26
+ timestamp: float = field(default_factory=time.time)
27
+ data: dict[str, Any] = field(default_factory=dict)
28
+ trace_id: str = ""
29
+ event_id: str = field(default_factory=lambda: uuid.uuid4().hex[:10])
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ return {
33
+ "kind": self.kind,
34
+ "step": self.step,
35
+ "timestamp": self.timestamp,
36
+ "data": self.data,
37
+ "trace_id": self.trace_id,
38
+ "event_id": self.event_id,
39
+ }
40
+
41
+
42
+ @dataclass
43
+ class Trace:
44
+ """
45
+ A complete execution trace for one task run.
46
+
47
+ Traces are immutable after finalization. They can be saved to JSONL
48
+ and reloaded for offline analysis, memory extraction, or replay.
49
+ """
50
+ trace_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
51
+ purpose: str = ""
52
+ run_mode: str = "learning_train"
53
+ started_at: float = field(default_factory=time.time)
54
+ finished_at: float = 0.0
55
+ events: list[TraceEvent] = field(default_factory=list)
56
+ metadata: dict[str, Any] = field(default_factory=dict)
57
+
58
+ def emit(self, kind: str, step: int = 0, **data) -> TraceEvent:
59
+ """Add a new event to the trace."""
60
+ event = TraceEvent(
61
+ kind=kind, step=step, data=data, trace_id=self.trace_id,
62
+ )
63
+ self.events.append(event)
64
+ return event
65
+
66
+ def finalize(self) -> None:
67
+ """Mark the trace as complete."""
68
+ self.finished_at = time.time()
69
+
70
+ @property
71
+ def duration_s(self) -> float:
72
+ if self.finished_at > 0:
73
+ return self.finished_at - self.started_at
74
+ return time.time() - self.started_at
75
+
76
+ @property
77
+ def step_count(self) -> int:
78
+ return max((e.step for e in self.events), default=0)
79
+
80
+ # --- JSONL persistence ---
81
+
82
+ def save(self, path: str) -> None:
83
+ """Save trace as JSONL (one event per line + header)."""
84
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
85
+ with open(path, "w") as f:
86
+ # Header line
87
+ header = {
88
+ "_type": "trace_header",
89
+ "trace_id": self.trace_id,
90
+ "purpose": self.purpose,
91
+ "run_mode": self.run_mode,
92
+ "started_at": self.started_at,
93
+ "finished_at": self.finished_at,
94
+ "metadata": self.metadata,
95
+ }
96
+ f.write(json.dumps(header, default=str) + "\n")
97
+ for event in self.events:
98
+ f.write(json.dumps(event.to_dict(), default=str) + "\n")
99
+
100
+ @classmethod
101
+ def load(cls, path: str) -> "Trace":
102
+ """Load a trace from JSONL file."""
103
+ with open(path) as f:
104
+ lines = [json.loads(line) for line in f if line.strip()]
105
+
106
+ if not lines:
107
+ return cls()
108
+
109
+ header = lines[0]
110
+ trace = cls(
111
+ trace_id=header.get("trace_id", ""),
112
+ purpose=header.get("purpose", ""),
113
+ run_mode=header.get("run_mode", "learning_train"),
114
+ started_at=header.get("started_at", 0),
115
+ finished_at=header.get("finished_at", 0),
116
+ metadata=header.get("metadata", {}),
117
+ )
118
+
119
+ for line in lines[1:]:
120
+ trace.events.append(TraceEvent(
121
+ kind=line.get("kind", ""),
122
+ step=line.get("step", 0),
123
+ timestamp=line.get("timestamp", 0),
124
+ data=line.get("data", {}),
125
+ trace_id=line.get("trace_id", ""),
126
+ event_id=line.get("event_id", ""),
127
+ ))
128
+ return trace
129
+
130
+ @classmethod
131
+ def load_many(cls, directory: str, glob: str = "*.jsonl") -> list["Trace"]:
132
+ """Load all traces from a directory."""
133
+ traces = []
134
+ for p in sorted(Path(directory).glob(glob)):
135
+ try:
136
+ traces.append(cls.load(str(p)))
137
+ except Exception as e:
138
+ logger.warning(f"Failed to load trace {p}: {e}")
139
+ return traces