Skip to content

Commit daba526

Browse files
Hert4claude
andcommitted
docs: Phase 7 slice 2 — mkdocs-material docs site
Full docs site with 10 pages covering the framework from first-touch to adapter contribution. Builds clean with `mkdocs build --strict`. - `mkdocs.yml`: Material theme with indigo palette, instant navigation, code-copy, light/dark toggle, pymdownx extensions (superfences, tabbed, tasklist, highlight with anchor line numbers). - `docs/index.md`: ASCII pipeline diagram + tabbed offline/semi-online modes + link matrix to the rest of the site. - `docs/getting-started.md`: Install, API key setup, Example 02 walkthrough, explicit Windows+non-ASCII-path caveat for the `PYTHONPATH` + `PYTHONIOENCODING` workaround we hit in session 12. - `docs/architecture.md`: The 4-axis table, 3-stage pipeline deep dive, data model, signal layer, async vs sync per plugin. - `docs/harnesses/{index,claude-code,langchain,custom}.md`: `custom.md` has a ~30-line skeleton a new harness can start from. - `docs/providers.md`: Every shipped provider + cost tracking + picking guidance table. Documents the `_inline_refs` and `_unstringify_json_fields` gotchas we had to solve for Gemini and Haiku respectively. - `docs/evidence.md`: Extract-vs-classify rule (plan §13.8), the known `next_user_turns=[]` gap for LangSmith. - `docs/rubrics.md`: No-regex rule, bilingual guidance. - `docs/{roadmap,contributing}.md`: Summaries linking the canonical files at repo root. - `[docs]` pyproject extra: mkdocs + mkdocs-material. - `.github/workflows/docs.yml`: Build on PR, deploy to gh-pages on push to main. Needs the "GitHub Pages source = GitHub Actions" toggle in repo settings to go live. - `/site/` gitignored. No code change. 319 tests still pass. plan.md §0.5 snapshot + Phase 7 row updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1e6d99b commit daba526

17 files changed

Lines changed: 1089 additions & 4 deletions

.github/workflows/docs.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: docs
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "docs/**"
8+
- "mkdocs.yml"
9+
- ".github/workflows/docs.yml"
10+
pull_request:
11+
branches: [main]
12+
paths:
13+
- "docs/**"
14+
- "mkdocs.yml"
15+
workflow_dispatch:
16+
17+
permissions:
18+
contents: read
19+
pages: write
20+
id-token: write
21+
22+
concurrency:
23+
group: docs-${{ github.ref }}
24+
cancel-in-progress: true
25+
26+
jobs:
27+
build:
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 0
33+
34+
- uses: actions/setup-python@v5
35+
with:
36+
python-version: "3.12"
37+
cache: pip
38+
39+
- name: Install docs extras
40+
run: pip install -e ".[docs]"
41+
42+
- name: Build
43+
run: mkdocs build --strict
44+
45+
- name: Upload pages artifact
46+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
47+
uses: actions/upload-pages-artifact@v3
48+
with:
49+
path: site/
50+
51+
deploy:
52+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
53+
needs: build
54+
runs-on: ubuntu-latest
55+
environment:
56+
name: github-pages
57+
url: ${{ steps.deploy.outputs.page_url }}
58+
steps:
59+
- id: deploy
60+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ Thumbs.db
4343
benchmarks/paper_replication/runs/
4444
benchmarks/paper_replication/workspaces/
4545

46+
# mkdocs build output
47+
/site/
48+
4649
# Local env / secrets
4750
.env
4851
.env.*

docs/architecture.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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.

docs/contributing.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Contributing
2+
3+
The canonical guide is [`CONTRIBUTING.md`](https://github.com/Hert4/trace2skill/blob/main/CONTRIBUTING.md) in the repo root. Key points:
4+
5+
## The three-check gate
6+
7+
Before you push, these must pass:
8+
9+
```bash
10+
python -m pytest tests/unit -q # 319 tests (or current count)
11+
ruff check .
12+
pyright --strict trace2skill
13+
```
14+
15+
CI runs the same three checks.
16+
17+
## Adding a new adapter
18+
19+
- Target **<300 LOC** per adapter. If you're going over, it probably belongs in core.
20+
- Lazy-import third-party SDKs inside method bodies; `TYPE_CHECKING` for type hints. Reason: keep core importable when the extra isn't installed.
21+
- Register in the axis's `__init__.py`, declare the optional dependency in `pyproject.toml`.
22+
- Test with mocks — no live API calls in `tests/unit/`.
23+
24+
Reference implementations:
25+
26+
| Axis | File |
27+
|---|---|
28+
| `HarnessAdapter` | [`trace2skill/harnesses/langchain.py`](https://github.com/Hert4/trace2skill/blob/main/trace2skill/harnesses/langchain.py) |
29+
| `EvidenceAdapter` | [`trace2skill/evidence_adapters/langchain.py`](https://github.com/Hert4/trace2skill/blob/main/trace2skill/evidence_adapters/langchain.py) |
30+
| `LLMProvider` | [`trace2skill/llm/openai_compatible.py`](https://github.com/Hert4/trace2skill/blob/main/trace2skill/llm/openai_compatible.py) |
31+
| `SkillFormat` | [`trace2skill/skill_formats/anthropic.py`](https://github.com/Hert4/trace2skill/blob/main/trace2skill/skill_formats/anthropic.py) |
32+
33+
## The no-regex rule (signal layer)
34+
35+
`trace2skill/signal/` must stay regex-free. All interpretation of evidence goes through the rubric + LLM judge. An ast-grep lint rule to enforce this is on the roadmap.
36+
37+
## Adapters extract, judges classify
38+
39+
An `EvidenceAdapter` pulls raw signals (user's next turn verbatim, tool errors as-is, git status text). It does **not** label success/failure or sentiment. The LLM judge + rubric YAML do that.
40+
41+
## Commit style
42+
43+
Imperative subject line, blank, body explaining *why*. Feature commits prefixed `feat:`, fixes `fix:`, docs `docs:`.
44+
45+
## Good first issues
46+
47+
See the [roadmap](roadmap.md) for the current "Near-term gaps" list.

docs/evidence.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Evidence adapters (semi-online mode)
2+
3+
Semi-online mode evolves the skill from **real** user sessions instead of benchmark tasks. An `EvidenceAdapter` extracts raw signals from one session; the LLM judge + rubric decides what those signals mean.
4+
5+
## Protocol
6+
7+
```python
8+
async def collect(self, session_id: str) -> Evidence: ...
9+
```
10+
11+
Where `Evidence` is:
12+
13+
```python
14+
@dataclass
15+
class Evidence:
16+
trajectory: Trajectory
17+
next_user_turns: list[str] # what the user said after, verbatim
18+
behavioral: dict[str, Any] # cwd, git status, duration, tokens, ...
19+
execution: dict[str, Any] # tool call counts, error counts
20+
explicit_feedback: dict | None # 👍/👎 if the platform tracks it
21+
session_metadata: dict # session id, timestamps, project name
22+
```
23+
24+
**Rule:** adapters extract, judges classify. Don't interpret sentiment or label success inside the adapter. The judge + rubric are the only place that decides y=0/1.
25+
26+
## Shipped adapters
27+
28+
### `ClaudeCodeEvidenceAdapter`
29+
30+
Given a Claude Code session UUID, finds the JSONL under `~/.claude/projects/<slug>/`, parses it, and extracts:
31+
32+
- `next_user_turns` — user messages after the first one (Claude Code's local-command + IDE-opened-file wrappers stripped)
33+
- `behavioral.cwd` + `behavioral.git_status_porcelain` if cwd is a git repo
34+
- `execution.tool_result_count` + `tool_error_count` + first few error messages
35+
36+
```python
37+
from trace2skill.evidence_adapters import ClaudeCodeEvidenceAdapter
38+
39+
adapter = ClaudeCodeEvidenceAdapter()
40+
evidence = await adapter.collect("abc-123-def-...")
41+
```
42+
43+
`projects_dir` defaults to `~/.claude/projects`; override if you keep sessions elsewhere.
44+
45+
### `LangChainEvidenceAdapter`
46+
47+
Backed by LangSmith (LangChain's tracing service). `session_id` = LangSmith run UUID.
48+
49+
- `trajectory` reconstructed by walking every descendant run of `run_type == "tool"` (agents nest tools inside sub-chains; flat inspection would miss them). Steps ordered by `start_time`.
50+
- `behavioral` — duration seconds, run_type, project name, token counts if present
51+
- `execution``tool_call_count`, `tool_error_count`, capped error snippets
52+
- `explicit_feedback``client.list_feedback(run_ids=[id])` parsed into a dict keyed by feedback key. Feedback API errors are swallowed (returns `None`) so evidence collection never fails on a flaky side channel.
53+
54+
```python
55+
from trace2skill.evidence_adapters import LangChainEvidenceAdapter
56+
from langsmith import Client
57+
58+
adapter = LangChainEvidenceAdapter(
59+
project_name="my-agent",
60+
client=Client(), # lazy-builds from LANGSMITH_API_KEY if None
61+
include_feedback=True,
62+
)
63+
evidence = await adapter.collect("run-uuid-...")
64+
```
65+
66+
Requires `pip install -e ".[langchain]"` (langsmith comes transitively with langchain).
67+
68+
#### Known gap
69+
70+
`next_user_turns` returns `[]`. LangSmith does not natively track "threads" of related runs — this would need a user-side convention (e.g. `extra.metadata.thread_id` on each run you emit, then a filter in `collect`). Adapter extension welcome.
71+
72+
## How evidence flows into the pipeline
73+
74+
```
75+
SessionStore (SQLite)
76+
session_id → unprocessed
77+
78+
EvidenceAdapter.collect()
79+
80+
Evidence
81+
82+
LLMJudge + Rubric YAML
83+
84+
Judgment (y, confidence)
85+
86+
Filter: keep y=1 trajectories (or y=0 if domain has reliable GT)
87+
88+
Trace2SkillPipeline.evolve_from_trajectories(...) ← skips Stage 1, goes straight to analyze + consolidate
89+
90+
AnthropicSkillFormat.save(evolved_skill, target_dir) ← atomic deploy + timestamped backup
91+
```
92+
93+
See `examples/03_claude_code_semi_online/` for a complete setup with Claude Code's `SessionEnd` hook.
94+
95+
## Writing your own
96+
97+
Implement one `async def collect` method. Keep it pure extraction. ~100-300 LOC is typical. Reference: `trace2skill/evidence_adapters/langchain.py`.

0 commit comments

Comments
 (0)