Skip to content

Repository files navigation

LatticeAG ForgeDistill 🔨

LicenseCIStarsIssuesTop languagePython

Correctness-by-construction distillation for agentic tool-calling models.
The tool-call chains are guaranteed correct before a single teacher token is spent.

Why This Exists · How It Works · Quick Start · Dataset Format · Verification


ForgeDistill (part of the LatticeAG Forge series - distillation, training, and models) is an open-source harness for generating high-structure training data for agentic tool-calling models - multi-turn, sequentially dependent tool calls with real error-recovery - using any teacher model, weak or strong.

Most distillation frameworks ask a teacher to demonstrate correct agentic behavior. Most models can't do that reliably, which is why so much synthetic agentic data has shallow one-call trajectories and fabricated values. ForgeDistill inverts the problem: the structure is built deterministically, the teacher only writes prose.

Built by LatticeAG.

Why This Exists

FrameworkApproachWeakness
distilabel (Argilla)General-purpose pipeline frameworkNo built-in correctness guarantees - you write your own steps
APIGen / xLAM (Salesforce)Post-hoc 3-stage verificationVerifies after generation, still throws away garbage, needs frontier teachers
Glaive function-callingGenerator-trusted, mostly single-turnNo dependency enforcement
AgentInstruct (Microsoft)Multi-agent flowsNo tool-dependency correctness

The differentiator: correctness by construction + grounding gates.

  1. We build the tool-call chain deterministically with real, unskippable dependencies - e.g. send_email(to="$0.result.email") where the address is an opaque value only obtainable by calling a prior tool. Guessing fails with a 400.
  2. The teacher is used only for prose (thoughts + final answer), never for tool calls - malformed JSON is impossible.
  3. Semantic grounding gates reject final answers that fabricate values not present in the real tool results.

Result: structurally-perfect, prose-grounded training traces from any OpenAI-compatible teacher endpoint.

How It Works

flowchart LR
A[Phase 1 - Chain Builder<br/>deterministic plan templates] -->|executed trajectory| B[Phase 2 - Teacher<br/>writes thoughts + final answer]
B -->|prose only| C[Gates<br/>format + grounding]
C -->|validated| D[Training trace<br/>reversed-v2 JSONL]
Loading

Phase 1 - deterministic chain construction (zero teacher tokens).agentic_plans.py picks a plan template (multi_hop, branch, recovery, join, fanout, reorder, digest, schema, idempotent, disambiguate, stop), fills variables, resolves $S.result references against prior step results, and executes against a deterministic mock executor. Sequential dependencies are unskippable by construction:

  • send_email(to="$0.result.email") - the teacher cannot guess the opaque address (e.g. carol.y.9024@internal.corp); it must come from a prior get_user result
  • db_query(...WHERE plan = '$0.result.plan') - the exact plan string must be learned from the user profile
  • Error-recovery plans deliberately fail the first step (bad id / bad city), then correct - teaching the observe-error-and-retry loop

Phase 2 - teacher prose (the only teacher spend). The teacher receives the full execution record (prompt + every call + exact results) and writes ONLY:

  • N <thought> blocks (reasoning before each call, including recovery thoughts)
  • FINAL_ANSWER: grounded in the real tool results

The harness assembles the final trace with its own validated tool calls. Weak models can do this; strong models do it better - both produce structurally-correct data.

Quick Start

# 1. Clone and set up
git clone https://github.com/LatticeAG/ForgeDistill.git
cd ForgeDistill
python3 -m venv .venv &&source .venv/bin/activate
pip install -e ".[dev]"# 2. Configure your multi-provider OpenAI-compatible roster (any OpenAI-compatible endpoints)
cp configs/roster.example.yaml configs/roster.yaml
# - set base_url / key_env per provider# - export your keys, e.g. export MY_PROVIDER_KEY=sk-...# 3. Sanity check the modules (no sys.path hacks after install)
python -c "import distill_tools, agentic_plans, prose_writer; print('OK')"# 4. Pilot run (10 traces)ulimit -n 65536
python src/distill_tools.py --count 10 --pilot
# or the installed console script:
distill --count 10 --pilot
# 5. Audit the output on disk (don't trust stdout)
cat data/raw/traces_*.jsonl | python -m json.tool --json-lines | head -20

After pip install -e ".[dev]", both forms work: python src/X.py for direct scripts and console entry points distill, eval_card, and export_sft for documented commands.

Generated traces land in data/raw/traces_<provider>.jsonl. The harness refuses to overwrite existing data - use src/archive_data.py to archive runs (it never deletes).

Dataset Format

Reversed-v2 format - each JSONL line:

{
"seed_class": "agentic",
"prompt": "...user prompt...",
"teacher": "provider/model",
"teacher_mode": "thinking|concise",
"plan_id": "plan_<template_id>",
"distill_version": "reversed-v2",
"forge_spec": "0.2",
"plan_tier": "easy",
"chain_steps": [
{"tool": "get_user", "args": {"user_id": 123}, "result": {"status": 200, "result": {...}}, "expect": "success"}
],
"eval": {
"format_ok": true,
"grounding_ok": true,
"chain_ok": true,
"dependency_ok": true,
"n_rounds": 2,
"n_tool_calls": 2,
"skills": ["multi_hop", "stop"],
"tier": "easy",
"repaired": false,
"cross_teacher": false
},
"messages": [
{"role": "system", "content": "..."},
{"role": "user", "content": "..."},
{"role": "assistant", "content": "<thought>...</thought>\n<tool_call>[{\"name\": ..., \"arguments\": {...}}]</tool_call>"},
{"role": "tool", "content": "{\"status\": 200, \"result\": {...}}"},
{"role": "assistant", "content": "final answer..."}
]
}
  • Assistant turns: thought + exact tool call (deterministic, validated JSON)
  • Tool turns: real executor results (success or error payloads)
  • No artificial "provide the final answer" nudge in exported messages
  • Final answer must contain key facts from the last successful tool result (grounding gate)

Verification

CI runs pytest -q on Python 3.11.

300-chain stress test (also stored as eval_card.jsoncommands.stress_300):

python -c "import sys,random; sys.path.insert(0,'src'); from agentic_plans import build_chain, validate_chain; r=random.Random(0); print(sum(1 for _ in range(300) if validate_chain(build_chain(r)['steps'])))"

Dataset (structural) audit

Latest measured run: 500 traces, multi-provider teacher fleet, reversed-v2.

python src/eval_card.py --input data/raw --out data/raw/eval_card.json --require-gates
MetricSource pathValue
Trace countn_traces500
Prose gate pass rategates.validate_prose_trace_pass1.0
Grounding gate pass rategates.validate_answer_grounding_pass1.0
Chain gate pass rategates.validate_chain_pass1.0
Dependency fidelity pass rategates.dependency_fidelity_pass1.0
Nudge leak rategates.nudge_leak_rate0.0
Malformed tool-call rategates.malformed_tool_call_rate0.0
Multi-round ratestructure.multi_round_rate0.988
Unique prompt ratestructure.unique_prompt_rate0.996
Unique trajectory hash ratestructure.unique_traj_hash_rate1.0
Send-email learned address ratestructure.send_email_learned_address_rate1.0
Cross-teacher split ratestructure.cross_teacher_split_rate0.0 (opt-in, off by default)
Cross-teacher fallback ratestructure.cross_teacher_fallback_rate0.0
300-chain invalidcommands.stress_300 stdout0 (300/300 valid)
Plan templates definedskills.n_templates_defined47
Skill tags definedskills.n_tags_defined15
Easy tier plansskills.tier_counts.easy9
Medium tier plansskills.tier_counts.medium12
Hard tier plansskills.tier_counts.hard21
Expert tier plansskills.tier_counts.expert5

--require-gates exits 0 on this run: prose, grounding, chain, and dependency fidelity are all 1.0 with zero nudge leaks. Every number above is copied from data/raw/eval_card.json - the file, not a hand-typed claim.

Historical note (2026-08-13, v0.1 pilot, n=10): 10/10 passed format + grounding gates; 100% multi-round; 0 malformed tool calls; 0 unique-prompt collisions; all send_email calls used in-context learned emails.

Plan counts source of truth:

python -c "from agentic_plans import PLANS, SKILLS; print(len(PLANS), len(SKILLS), sorted(SKILLS))"

Forge Live Tool Eval (student), not BFCL v3

CategoryScoreCommand
multipleno student checkpoint in this tagpython src/eval_live.py --endpoint $STUDENT_URL --model $STUDENT_MODEL --holdout data/raw/holdout_plan_ids.json --n 50 --out data/raw/live_eval.json --key-env STUDENT_KEY_ENV
parallelno student checkpoint in this tagsame as above
multi_turnno student checkpoint in this tagsame as above
irrelevanceunscoredsame as above

CI uses eval_live.py --replay (ReplayStudent) separately; those scores are not student-checkpoint rows in this table.

Export quickstart

python src/export_sft.py \
--input data/raw \
--out data/export/nanbeige.jsonl \
--template configs/templates/nanbeige.json \
--format messages \
--check-mask

configs/templates/nanbeige.json and configs/templates/chatml.json set assistantloss: true and all other roles loss: false. --check-mask validates message-level assistant-only spans. Token-level --check-tokenizer is pending a published NANBEIGE_TOKENIZER checkpoint and an optional transformers install (not a default dependency); do not claim tokenizer verification until that env is set. The command above writes data/export/nanbeige.jsonl; data/export/mask_audit.txt records the first 3 traj_hash values and per-example trainable-span counts from that run. Do not type span counts by hand.

Curriculum

--curriculum {off,uniform,linear} on distill:

  • uniform: 0.25 weight per tier (easy / medium / hard / expert)
  • linear: mix_for_progress at progress 0: easy 0.50, medium 0.30, hard 0.15, expert 0.05; at progress 1: easy 0.10, medium 0.20, hard 0.40, expert 0.30

--holdout-frac 0.15 writes holdout_plan_ids.json beside traces. Pilots use --holdout-frac 0.

Coverage note from eval_card.COVERAGE_NOTE: skills.coverage is computed against train_ids only; holdout_plan_ids lists excluded ids so readers can reproduce. Tier coverage assertions in CI run with --holdout-frac 0 fixtures.

Features

CategoryFeature
Structure47 plans, 15 tags: arithmetic, branch, calendar, digest, disambiguate, fanout, idempotent, join, multi_hop, nested, recovery, reorder, schema, search, stop. Tiers: easy 9, medium 12, hard 21, expert 5
Dependencies$S.result.field refs create unskippable sequential dependencies
RecoveryDeliberate failure + correction plans teach observe-error-and-retry
GroundingSemantic gate rejects fabricated values in final answers
Teacher-agnosticMulti-provider teacher fleet - any OpenAI-compatible endpoint
ResiliencePer-provider health state machine (healthy / backoff / quarantined), 429 quarantine, Retry-After honoring, exponential backoff with jitter
Fleet managementPer-provider semaphores, concurrency scaling, weighted model sampling, dead-route re-probing
RobustnessTrajectory-hash dedup, checkpoint/resume per provider, per-worker RNG, token accounting
SafetyNever overwrites existing data; archive-before-run; refuses --wipe unless explicit
Curriculum--curriculum {off,uniform,linear} tier mixing
Exportexport_sft.py renders SFT jsonl with assistant-only loss masks
DPOdpo_pairs.py offline preference pairs from assembled traces
Eval cardeval_card.py --require-gates structural audit json
Live hookeval_live.py Forge Live Tool Eval (ReplayStudent in CI)

Repository Layout

src/agentic_plans.py Phase 1: deterministic chain builder + plan templates + mock executor
src/prose_writer.py Phase 2: teacher prose contract + format/grounding validators + trace assembly
src/distill_tools.py Async worker loop: fleet health, semaphores, checkpointing, CLI
src/mock_tools.py Shared deterministic tool executor (11 tools)
src/archive_data.py Archive data/raw to data/archive/<timestamp>_<label>/ - never deletes
src/eval_card.py Structural eval card json from traces_*.jsonl
src/export_sft.py SFT export with template-driven loss masks
src/eval_live.py Forge Live Tool Eval student hook
src/verifier.py Optional LLM verifier for prose repair
src/curriculum.py Tier mix, holdout split, plan picking
src/dpo_pairs.py Offline DPO pair builder
configs/templates/*.json Chat templates (nanbeige, chatml)
configs/roster.example.yaml Teacher fleet config template (copy to roster.yaml)
tests/ pytest suite
.github/workflows/ci.yml CI: editable install, pytest, CLI --help smoke
pyproject.toml Package metadata and console scripts
safe_launch.sh Archive-first launcher with raised fd limit

Publishing

Export HF_TOKEN for the commands below; do not rely on a cached huggingface-cli login; if unset at tag time the upload is a later operator step.

huggingface-cli upload LatticeAG/ForgeDistill-agentic data/raw/eval_card.json --repo-type dataset
huggingface-cli upload LatticeAG/ForgeDistill-agentic data/export/nanbeige.jsonl --repo-type dataset --path-in-repo sft/nanbeige.jsonl
huggingface-cli upload LatticeAG/ForgeDistill-agentic data/raw/dpo_pairs_offline.jsonl --repo-type dataset --path-in-repo dpo/dpo_pairs.jsonl

Include holdout_plan_ids.json on the dataset card so the 0.15 holdout split is visible to consumers. Operators may upload it alongside eval_card.json:

huggingface-cli upload LatticeAG/ForgeDistill-agentic data/raw/holdout_plan_ids.json --repo-type dataset

Configure teachers via a multi-provider OpenAI-compatible roster (configs/roster.yaml); keys live in env vars only.

Locked design decisions (v0.3)

search.query. Intersection over whitespace tokens; hit order is DOC_BY_QUERY[tokens[0]]; empty and unknown tokens behave as implemented. Do not change this semantics.

price. Price values come from VAR_POOLS["price"] = [20, 35, 50], not a CRM field.

Opaque ids. Identifiers such as evt.* and doc.* may be quoted when they appeared in tool payloads; inventing one fails grounding; students are not required to recite them.

roles.answer pin. A roster pin for roles.answer wins over sampling; rate still gates whether a split happens; sampling never overrides a valid pin.

--mp token accounting. Multiprocess runs write .token_usage_{shard_i}.json sidecars merged by the parent; do not publish all-zero token totals on multiprocess runs.

License

Released under the MIT License. Built by LatticeAG.

About

Correctness-by-construction distillation for agentic tool-calling models (LatticeAG Forge series)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages