|
| 1 | +# Architecture |
| 2 | + |
| 3 | +The framework is built around one principle: **core never imports an adapter or a provider SDK**. Everything flows through four `Protocol` classes defined in [`trace2skill/core/protocols.py`](https://github.com/Hert4/trace2skill/blob/main/trace2skill/core/protocols.py). Users mix and match. |
| 4 | + |
| 5 | +## The 4 plugin axes |
| 6 | + |
| 7 | +``` |
| 8 | +┌──────────────────────────────────────────────────────────────┐ |
| 9 | +│ trace2skill CORE │ |
| 10 | +│ (harness-agnostic, zero domain logic) │ |
| 11 | +│ │ |
| 12 | +│ Stage 1 Rollout → Stage 2 Analyze → Stage 3 Merge │ |
| 13 | +│ + Signal Layer │ |
| 14 | +└───┬──────────────┬──────────────┬──────────────┬─────────────┘ |
| 15 | + │ │ │ │ |
| 16 | +┌───▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼──────┐ |
| 17 | +│Harness │ │ LLM │ │ Skill │ │ Evidence │ |
| 18 | +│Adapter │ │Provider │ │ Format │ │ Adapter │ |
| 19 | +└────────┘ └─────────┘ └─────────┘ └───────────┘ |
| 20 | +``` |
| 21 | + |
| 22 | +| Axis | What it does | Shipped reference implementations | |
| 23 | +|---|---|---| |
| 24 | +| `HarnessAdapter` | `async run_task(query, skill_dir, workspace, turn_budget) -> Trajectory`. Runs one task on an agent. | [Claude Code](harnesses/claude-code.md) (subprocess), [LangChain](harnesses/langchain.md) (AgentExecutor), SimpleReAct (in-process) | |
| 25 | +| `LLMProvider` | `complete` / `complete_structured` / `react`. Wraps one LLM API for the analyst, merger, and judge. | `AnthropicLLMProvider`, `OpenAICompatibleProvider` (generic — covers OpenAI, Gemini, OpenRouter, DeepSeek, Groq, Together, xAI) | |
| 26 | +| `SkillFormat` | `load(path) -> Skill` / `save(skill, path)`. On-disk skill representation. | `AnthropicSkillFormat` (`SKILL.md + resources/`) | |
| 27 | +| `EvidenceAdapter` | `async collect(session_id) -> Evidence`. Extracts raw signals from a real session. | `ClaudeCodeEvidenceAdapter` (JSONL sessions), `LangChainEvidenceAdapter` (LangSmith runs) | |
| 28 | + |
| 29 | +Two more plugin-adjacent types: |
| 30 | + |
| 31 | +- `Evaluator` — domain-specific scoring for batch mode. One is shipped: `SpreadsheetEvaluator` for the paper-replication benchmark. |
| 32 | +- `Rubric` — YAML-driven judge criteria (signal layer). One is shipped: `rubrics/generic.yaml`. |
| 33 | + |
| 34 | +## The 3-stage pipeline |
| 35 | + |
| 36 | +### Stage 1 — Rollout |
| 37 | + |
| 38 | +N tasks run in parallel through a `HarnessAdapter`. Defaults: `rollout_workers=32`. Each rollout produces a `Trajectory` (query, steps, final answer, artifacts). The `Evaluator` labels `y ∈ {0, 1}`. |
| 39 | + |
| 40 | +Async `asyncio.Semaphore(workers)` gating, JSONL checkpoint written after `gather()`. |
| 41 | + |
| 42 | +### Stage 2 — Analyze |
| 43 | + |
| 44 | +For every trajectory: |
| 45 | + |
| 46 | +- `y = 1` → `SuccessAnalyst` (single-pass, structured output). |
| 47 | +- `y = 0` → `AgenticErrorAnalyst` — ReAct loop with 6 tools: |
| 48 | + - `inspect_skill_file(path)` — show a skill file with line numbers |
| 49 | + - `read_ground_truth()` — show the expected answer (includes xlsx cell dump for spreadsheet domain) |
| 50 | + - `try_patch(ops)` — dry-run a candidate patch, return unified diff or validation error |
| 51 | + - `diff_vs_gt()` — compare trajectory output to ground truth |
| 52 | + - `finish_with_patch(ops, rationale)` — emit verified fix |
| 53 | + - `drop(reason)` — quality gate: cannot verify cause, drop trajectory |
| 54 | + |
| 55 | +Parallel dispatch (`analyst_workers=32` default; paper uses 128). `analyst_modes={"error"}` / `{"success"}` / both reproduces paper's +Error / +Success / +Combined conditions. |
| 56 | + |
| 57 | +!!! note "Why agentic" |
| 58 | + Paper §4.3 shows single-LLM-call analysts propose surface-level edits ("be more careful"). The agentic loop can actually run candidate patches, diff against ground truth, and propose domain-specific fixes. This is Trace2Skill's main USP vs concurrent approaches. |
| 59 | + |
| 60 | +### Stage 3 — Consolidate |
| 61 | + |
| 62 | +Three deterministic guardrails drop bad patches before any LLM sees them: |
| 63 | + |
| 64 | +1. **File existence** — target file must exist in the frozen skill |
| 65 | +2. **Line-range conflict** — hunks within one patch must not overlap |
| 66 | +3. **Trial apply** — dry-run `Skill.apply_patch` on the base skill, reject if it raises |
| 67 | + |
| 68 | +Surviving patches feed a **hierarchical merge** (batch size 32, max depth 6 by default). Merge prompt instructs the LLM to keep only edits appearing ≥2 times across the pool — prevalence-weighted induction from paper §2.4. |
| 69 | + |
| 70 | +Output: one final `Patch` + the complete provenance chain (`source_traj_ids` for every op). Apply to the seed skill, save, done. |
| 71 | + |
| 72 | +## Data model at a glance |
| 73 | + |
| 74 | +```python |
| 75 | +@dataclass |
| 76 | +class Task: |
| 77 | + task_id: str |
| 78 | + query: str |
| 79 | + inputs: dict[str, Path] |
| 80 | + ground_truth: GroundTruth # File | Value | Callable |
| 81 | + metadata: dict |
| 82 | + |
| 83 | +@dataclass |
| 84 | +class Trajectory: |
| 85 | + task_id: str |
| 86 | + query: str |
| 87 | + steps: list[ReActStep] |
| 88 | + final_answer: str |
| 89 | + y: int | None # 0 | 1, set by Evaluator |
| 90 | + artifacts: dict[str, Path] |
| 91 | + model: str |
| 92 | + metadata: dict |
| 93 | + |
| 94 | +@dataclass |
| 95 | +class Skill: |
| 96 | + root_md: str # SKILL.md content |
| 97 | + resources: dict[str, bytes] # {relative_path: content} |
| 98 | + |
| 99 | + def freeze(self) -> FrozenSkill: ... |
| 100 | + def apply_patch(self, patch: Patch) -> Skill: ... |
| 101 | +``` |
| 102 | + |
| 103 | +Full definitions: [`trace2skill/core/models.py`](https://github.com/Hert4/trace2skill/blob/main/trace2skill/core/models.py). |
| 104 | + |
| 105 | +## Signal layer (semi-online mode) |
| 106 | + |
| 107 | +``` |
| 108 | +EvidenceAdapter.collect(session_id) ──→ Evidence (raw) |
| 109 | + ↓ |
| 110 | + LLMJudge |
| 111 | + + Rubric YAML |
| 112 | + ↓ |
| 113 | + Judgment (y, confidence, failure_modes) |
| 114 | + ↓ |
| 115 | + Pipeline.evolve_from_trajectories(...) |
| 116 | +``` |
| 117 | + |
| 118 | +Rule of thumb (plan §13.8): **adapters extract, judges classify**. Never preprocess sentiment or categorize evidence inside the adapter — the judge + rubric decide what the signals mean. |
| 119 | + |
| 120 | +## Async choices |
| 121 | + |
| 122 | +| Plugin | Sync or async | |
| 123 | +|---|---| |
| 124 | +| `HarnessAdapter.run_task` | async (I/O) | |
| 125 | +| `LLMProvider.*` | async (I/O) | |
| 126 | +| `EvidenceAdapter.collect` | async (I/O) | |
| 127 | +| `SkillFormat.load` / `save` | sync (local files) | |
| 128 | +| `Evaluator.evaluate` | sync (pure computation) | |
| 129 | + |
| 130 | +Pipeline and CLI run on a single asyncio event loop. Use `asyncio.to_thread(...)` when wrapping a sync third-party SDK. |
0 commit comments