Skip to content

Commit a039853

Browse files
EightRiceclaude
andcommitted
Add P2P wiring, trace logger, integration tests, gate experiments, and constitutional geometry
- trace_logger.py: Structured agent trace collection for training data - constitutional_geometry.py: Geometric alignment evaluation in embedding space - test_integration.py: End-to-end pipeline integration tests - gate1_probe.py: Gate 1 probe experiment (>85% tool call prediction) - gate2_degradation.py: Gate 2 degradation curves (<50% quality drop at T=5) - P2P wiring: blob_store, distributed_jepa, sharding, p2p, governance extensions - Config updates for trace logging and P2P inference Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d73ce59 commit a039853

16 files changed

Lines changed: 3902 additions & 62 deletions

atn/config.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,27 @@ class AutonetConfig:
111111
wallet_address: str = "" # Connected wallet address (empty = not connected)
112112

113113

114+
@dataclass
115+
class TraceLoggingConfig:
116+
"""Configuration for structured agent trace logging.
117+
118+
Traces are written to ``trace_dir`` (default: ``{data_dir}/traces/``) as
119+
content-addressed JSON files and serve as training data for VL-JEPA.
120+
121+
Example config.yaml snippet::
122+
123+
trace_logging:
124+
enabled: true
125+
trace_dir: ~/.atn/traces # optional override
126+
include_user_data: false # set true to include orchestrator sessions
127+
min_turns: 1 # quality filter: skip near-empty sessions
128+
"""
129+
enabled: bool = False
130+
trace_dir: str = "" # empty → {data_dir}/traces/
131+
include_user_data: bool = False # consent gate (Epic 3 Story 3.1)
132+
min_turns: int = 1 # minimum assistant turns for quality filter
133+
134+
114135
@dataclass
115136
class ATNConfig:
116137
"""Top-level ATN configuration."""
@@ -119,6 +140,7 @@ class ATNConfig:
119140
orchestrator: OrchestratorConfig = field(default_factory=OrchestratorConfig)
120141
voice: VoiceConfig = field(default_factory=VoiceConfig)
121142
autonet: AutonetConfig = field(default_factory=AutonetConfig)
143+
trace_logging: TraceLoggingConfig = field(default_factory=TraceLoggingConfig)
122144
providers: dict[str, ProviderConfig] = field(default_factory=dict)
123145
connectors: dict[str, ConnectorConfig] = field(default_factory=dict)
124146
raw: dict[str, Any] = field(default_factory=dict)
@@ -272,6 +294,16 @@ def load_config(path: Path | None = None) -> ATNConfig:
272294
wallet_address=resolved.get("wallet_address", ""),
273295
)
274296

297+
# Trace logging
298+
trace_raw = raw.get("trace_logging", {})
299+
if isinstance(trace_raw, dict):
300+
config.trace_logging = TraceLoggingConfig(
301+
enabled=trace_raw.get("enabled", False),
302+
trace_dir=trace_raw.get("trace_dir", ""),
303+
include_user_data=trace_raw.get("include_user_data", False),
304+
min_turns=trace_raw.get("min_turns", 1),
305+
)
306+
275307
# Connectors
276308
for name, craw in raw.get("connectors", {}).items():
277309
if not isinstance(craw, dict):

atn/conversation.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,29 @@ def turn_count(self) -> int:
168168
"""Number of turns in the active conversation."""
169169
return len(self._turns)
170170

171+
def export_trace_turns(self) -> list[dict[str, Any]]:
172+
"""Export the active conversation as structured trace turns.
173+
174+
Converts the stored conversation history to the trace format used by
175+
TraceLogger and VL-JEPA training:
176+
177+
[
178+
{"role": "user", "content": "...", "tool_calls": [], "timestamp": "..."},
179+
{"role": "assistant", "content": "...", "tool_calls": [], "timestamp": "..."},
180+
]
181+
182+
Returns an empty list if there are no turns.
183+
"""
184+
return [
185+
{
186+
"role": turn.role,
187+
"content": turn.content,
188+
"tool_calls": [],
189+
"timestamp": turn.timestamp.isoformat(),
190+
}
191+
for turn in self._turns
192+
]
193+
171194
# ------------------------------------------------------------------
172195
# Write
173196
# ------------------------------------------------------------------

atn/runtime/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,26 @@ def __init__(self, event_bus: EventBus, data_dir: Path | None = None, config: AT
202202
data_dir=str(self._config.data_dir),
203203
)
204204

205+
# Trace logger (Phase B Step 1 — Agent Trace Collection)
206+
from ..trace_logger import TraceLogger, TraceLoggingConfig as _TLConfig
207+
_tl_cfg = self._config.trace_logging
208+
_trace_dir = (
209+
Path(_tl_cfg.trace_dir).expanduser()
210+
if _tl_cfg.trace_dir
211+
else self._config.data_dir / "traces"
212+
)
213+
self.trace_logger = TraceLogger(
214+
config=_TLConfig(
215+
enabled=_tl_cfg.enabled,
216+
trace_dir=str(_trace_dir),
217+
include_user_data=_tl_cfg.include_user_data,
218+
min_turns=_tl_cfg.min_turns,
219+
),
220+
trace_dir=_trace_dir,
221+
)
222+
# Wire trace_logger into the execution engine
223+
self.engine.trace_logger = self.trace_logger
224+
205225
# Tool registry
206226
from ..tool_registry import ToolRegistry
207227
self.tool_registry = ToolRegistry(self)
@@ -260,6 +280,9 @@ async def start(self) -> None:
260280
if self._config.autonet.enabled:
261281
await self.autonet.start()
262282

283+
# Attach trace logger to event bus (subscribes to AGENT_TOOL_USE_* events)
284+
self.trace_logger.attach(self.events)
285+
263286
await self.events.emit(Event(
264287
type=EventType.RUNTIME_STARTED,
265288
source="runtime",

atn/runtime/execution_engine.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ def __init__(
234234
# Events for signalling delegate completion
235235
self._delegate_done: dict[str, asyncio.Event] = {}
236236

237+
# Trace logger (set by Runtime after construction; may be None)
238+
self.trace_logger: Any = None
239+
237240
# ------------------------------------------------------------------
238241
# Trigger
239242
# ------------------------------------------------------------------
@@ -668,6 +671,17 @@ async def _on_chunk(text: str) -> None:
668671
},
669672
))
670673

674+
# --- Trace logging: record execution start ---
675+
if self.trace_logger is not None:
676+
self.trace_logger.begin_execution(
677+
agent_id=defn.id,
678+
execution_id=record.execution_id,
679+
agent_type=defn.agent_type or ("orchestrator" if is_orchestrator else "cognitive"),
680+
system_prompt=system_prompt,
681+
# Store the raw user message before history prepending for cleaner traces
682+
user_message="\n\n".join(prompt_parts) if prompt_parts else (defn.description or defn.name),
683+
)
684+
671685
# --- Run the agent ---
672686
send_kwargs: dict[str, Any] = {
673687
"message": user_message,
@@ -741,6 +755,22 @@ async def _on_chunk(text: str) -> None:
741755
execution_id=record.execution_id,
742756
))
743757

758+
# --- Trace logging: finalise trace ---
759+
if self.trace_logger is not None:
760+
_trace_result = ""
761+
if isinstance(record.output, dict):
762+
_trace_result = record.output.get("result", "")
763+
elif record.output:
764+
_trace_result = str(record.output)
765+
self.trace_logger.end_execution(
766+
agent_id=defn.id,
767+
execution_id=record.execution_id,
768+
result_text=_trace_result,
769+
status=record.status.value,
770+
error=record.error,
771+
completed_at=record.completed_at,
772+
)
773+
744774
# Record assistant turn (children)
745775
if not is_orchestrator:
746776
_convo_text = ""

0 commit comments

Comments
 (0)