archforge-optimizer 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. archforge_optimizer-0.4.0/.gitignore +6 -0
  2. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/PKG-INFO +90 -74
  3. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/README.md +85 -72
  4. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/cli.py +145 -5
  5. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/config.py +17 -1
  6. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/config_init.py +7 -0
  7. archforge_optimizer-0.4.0/archforge/judge/__init__.py +56 -0
  8. archforge_optimizer-0.4.0/archforge/judge/deepeval.py +236 -0
  9. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/llm/__init__.py +15 -22
  10. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/llm/base.py +30 -2
  11. archforge_optimizer-0.4.0/archforge/llm/litellm.py +146 -0
  12. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/userconfig.py +1 -2
  13. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/pyproject.toml +7 -4
  14. archforge_optimizer-0.2.0/.gitignore +0 -12
  15. archforge_optimizer-0.2.0/archforge/judge/__init__.py +0 -20
  16. archforge_optimizer-0.2.0/archforge/llm/_common.py +0 -94
  17. archforge_optimizer-0.2.0/archforge/llm/anthropic.py +0 -90
  18. archforge_optimizer-0.2.0/archforge/llm/gemini.py +0 -112
  19. archforge_optimizer-0.2.0/archforge/llm/groq.py +0 -63
  20. archforge_optimizer-0.2.0/archforge/llm/openai.py +0 -63
  21. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/LICENSE +0 -0
  22. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/__init__.py +0 -0
  23. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/__main__.py +0 -0
  24. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/architect.py +0 -0
  25. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/diff.py +0 -0
  26. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/engine.py +0 -0
  27. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/gatekeeper.py +0 -0
  28. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/host/__init__.py +0 -0
  29. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/host/adapters/__init__.py +0 -0
  30. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/host/adapters/base.py +0 -0
  31. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/host/adapters/helpers.py +0 -0
  32. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/host/adapters/langgraph.py +0 -0
  33. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/host/base.py +0 -0
  34. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/host/fake.py +0 -0
  35. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/judge/base.py +0 -0
  36. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/judge/scripted.py +0 -0
  37. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/lint.py +0 -0
  38. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/llm/scripted.py +0 -0
  39. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/middleware.py +0 -0
  40. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/models.py +0 -0
  41. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/mutate.py +0 -0
  42. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/otel.py +0 -0
  43. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/runlog.py +0 -0
  44. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/runner.py +0 -0
  45. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/spec_builder.py +0 -0
  46. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/stores/__init__.py +0 -0
  47. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/stores/_jsonl.py +0 -0
  48. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/stores/attempt_store.py +0 -0
  49. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/stores/spec_store.py +0 -0
  50. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/stores/trace_store.py +0 -0
  51. {archforge_optimizer-0.2.0 → archforge_optimizer-0.4.0}/archforge/suite.py +0 -0
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ .pytest_cache/
3
+ data/
4
+ *.db
5
+ .env
6
+
@@ -1,12 +1,15 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: archforge-optimizer
3
- Version: 0.2.0
4
- Summary: ArchForge — a self-improving meta-layer over multi-agent systems
3
+ Version: 0.4.0
4
+ Summary: ArchForge: a self-improving meta-layer over multi-agent systems
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.11
8
+ Requires-Dist: litellm
8
9
  Requires-Dist: pydantic>=2.7
9
10
  Requires-Dist: python-dotenv>=1.0
11
+ Provides-Extra: deepeval
12
+ Requires-Dist: deepeval; extra == 'deepeval'
10
13
  Provides-Extra: dev
11
14
  Requires-Dist: hypothesis>=6; extra == 'dev'
12
15
  Requires-Dist: mypy>=1.11; extra == 'dev'
@@ -40,20 +43,28 @@ Description-Content-Type: text/markdown
40
43
  </p>
41
44
 
42
45
  > A self-improving meta-layer over multi-agent systems.
43
- > Point it at your graph, give it a rubric, and it evolves your pipeline — one proven change per cycle.
46
+ > Point it at your graph, give it a rubric, and it evolves your pipeline, one proven change per cycle.
44
47
 
45
- **Minimal core dependencies · optional provider and tracing integrations** · install from PyPI with `pip install archforge-optimizer` · exercised end-to-end on a real LangGraph MAS (groq + google-genai + chroma, OpenTelemetry-traced).
48
+ **Minimal core dependencies** · **optional provider and tracing integrations** · install from PyPI with `pip install archforge-optimizer` · exercised end-to-end on a real LangGraph MAS (groq + google-genai + chroma, OpenTelemetry-traced).
46
49
 
47
- ArchForge sits **on top** of an existing multi-agent system (MAS) and improves it run-over-run. Each cycle it inspects where the judge docked points, proposes **one** targeted change — rewriting an agent's prompt, tuning a knob, adding a verifier, re-wiring a node, swapping a model — and keeps it only if it measurably beats the incumbent on a held-out suite. The host MAS keeps running tasks as normal; ArchForge observes the runs and feeds back an improved pipeline.
50
+ ## What is ArchForge?
51
+
52
+ ArchForge sits **on top** of an existing multi-agent system (MAS) and improves it over time. In plain terms:
53
+
54
+ 1. **Look.** It inspects where the judge docked points on the last run.
55
+ 2. **Propose.** It proposes **one** targeted change: rewriting an agent's prompt, tuning a knob, adding a verifier, rewiring a node, or swapping a model.
56
+ 3. **Keep or drop.** It keeps the change only if it measurably beats the current pipeline on a held-out suite.
57
+
58
+ Your MAS keeps running tasks as normal. ArchForge watches those runs and feeds back an improved pipeline.
48
59
 
49
60
  The design is deliberately minimal and verifiable:
50
61
 
51
- - **One protected incumbent.** A candidate never touches production config; it's promoted only when its mean score beats the incumbent's by at least the margin τ.
52
- - **Immutable, versioned Specs are the single source of truth.** Evolving the pipeline = swapping which Spec the host instantiates, never patching live state.
53
- - **Hybrid autonomy.** Safe small edits (prompt/knob) auto-apply; structural edits (roster/graph/model) queue for human approval.
54
- - **Observation/control asymmetry.** The wrapper records traces and reports the active Spec, but never rewrites prompts mid-run. All mutation happens *between* runs, on the Spec.
62
+ - **One protected incumbent.** A candidate never touches production config. It is promoted only when its mean score beats the incumbent's by at least the margin τ.
63
+ - **Immutable, versioned Specs are the single source of truth.** Evolving the pipeline means swapping which Spec the host instantiates, never patching live state.
64
+ - **Hybrid autonomy.** Safe small edits (prompt/knob) apply automatically. Structural edits (roster/graph/model) queue for human approval.
65
+ - **Observation/control asymmetry.** The wrapper records traces and reports the active Spec, but never rewrites prompts mid-run. All mutation happens between runs, on the Spec.
55
66
 
56
- No ground truth is required — an LLM-as-judge scores each run against a rubric.
67
+ No ground truth is required. An LLM-as-judge scores each run against a rubric.
57
68
 
58
69
  ### At a glance
59
70
 
@@ -65,16 +76,16 @@ No ground truth is required — an LLM-as-judge scores each run against a rubric
65
76
  | [**Judge**](archforge/judge/) | LLM-as-judge: scores each run per a versioned rubric, with a per-step breakdown for credit assignment |
66
77
  | [**Gatekeeper**](archforge/gatekeeper.py) | Decides promote / queue-for-human / discard / rollback by margin `τ` + scope |
67
78
  | [**Stores**](archforge/stores/) | `SpecStore` (versioned, content-addressed) + `TraceStore` + `AttemptStore` (all append-only) |
68
- | [**TracingMiddleware**](archforge/middleware.py) | The host seam — wraps every agent, records each `Step`, reports the active Spec |
79
+ | [**TracingMiddleware**](archforge/middleware.py) | The host seam: wraps every agent, records each `Step`, reports the active Spec |
69
80
 
70
81
  ---
71
82
 
72
83
  ## A Simple Example
73
84
 
74
- One Propose-Evaluate-Commit cycle, **zero cost** — no LLM, no network, no API keys. It wires the scripted organs (a fake host, a scripted Architect that proposes one prompt edit, a scripted Judge that scores it a win) into the real `Engine`, and you watch an auto-promotion end-to-end. To run it for real on your own MAS, replace the scripted organs: pass `--adapter your_pkg.your_host:YourAdapter` and `--provider <llm>` to `archforge-optimizer evolve` (see [Quickstart](#quickstart)).
85
+ One Propose-Evaluate-Commit cycle, **zero cost**: no LLM, no network, no API keys. It wires the scripted organs (a fake host, a scripted Architect that proposes one prompt edit, a scripted Judge that scores it a win) into the real `Engine`, and you watch an auto-promotion end-to-end. To run it for real on your own MAS, replace the scripted organs with `--adapter your_pkg.your_host:YourAdapter` and `--provider <llm>` on `archforge-optimizer evolve` (see [Quickstart](#quickstart)).
75
86
 
76
87
  ```python
77
- # save this as evolve_demo.py — run from an `init`-ed project dir
88
+ # save this as evolve_demo.py, then run it from an init-ed project dir
78
89
  import tempfile
79
90
  from pathlib import Path
80
91
 
@@ -108,12 +119,12 @@ with tempfile.TemporaryDirectory() as d:
108
119
  specs, atts, ts = SpecStore(root_dir), AttemptStore(root_dir), TraceStore(root_dir)
109
120
  rid = specs.commit(seed, parent_spec_id=None, status=m.SpecStatus.INCUMBENT)
110
121
  specs.set_active(rid)
111
- # the candidate's spec_id is content-hashed OVER its parent — mirror the
122
+ # the candidate's spec_id is content-hashed OVER its parent, so mirror the
112
123
  # engine's commit (parent = rid) when scripting the judge's score for it
113
124
  cand.parent_spec_id = rid
114
125
  judge = (ScriptedJudge(rubric=rubric)
115
126
  .set_aggregate(rid, "t1", 0.55) # incumbent baseline
116
- .set_aggregate(cand.compute_spec_id(), "t1", 0.70)) # +0.15 >= τ
127
+ .set_aggregate(cand.compute_spec_id(), "t1", 0.70)) # +0.15 >= tau
117
128
 
118
129
  engine = Engine(
119
130
  host=FakeHostMAS(), judge=judge, architect=ScriptedArchitect().propose(change, {"prompt": "p1"}),
@@ -128,14 +139,14 @@ with tempfile.TemporaryDirectory() as d:
128
139
  ```
129
140
 
130
141
  ```
131
- archforge-optimizer init # once — scaffolds project config + the archforge_optimizer/ adapter package
142
+ archforge-optimizer init # once: scaffolds project config + the archforge_optimizer/ adapter package
132
143
  python evolve_demo.py
133
144
  action=AUTO_PROMOTE margin=+0.15
134
145
  incumbent_mean=0.55 candidate_mean=0.70
135
146
  active_spec_id=57396a49 promoted=True
136
147
  ```
137
148
 
138
- The candidate's tighter prompt beat the incumbent by `+0.15 ≥ τ`, so the Gatekeeper **auto-promoted** it to the new active Spec — all within immutable, versioned storage. Nothing was patched in place; the host will now instantiate the new Spec on its next run.
149
+ The candidate's tighter prompt beat the incumbent by `+0.15 ≥ τ`, so the Gatekeeper **auto-promoted** it to the new active Spec, all within immutable, versioned storage. Nothing was patched in place; the host will now instantiate the new Spec on its next run.
139
150
 
140
151
  ---
141
152
 
@@ -176,7 +187,7 @@ The candidate's tighter prompt beat the incumbent by `+0.15 ≥ τ`, so the Gate
176
187
  │ small + win → auto-promote │ structural + win → human gate │
177
188
  │ lose → discard │ regress → rollback (lineage) │
178
189
  │ ▼ │
179
- │ SpecStore (versioned, immutable) — "active incumbent" pointer │
190
+ │ SpecStore (versioned, immutable) - "active incumbent" pointer │
180
191
  └─────────────────────────────┬──────────────────────────────────┘
181
192
  └── next host runs use the new incumbent Spec
182
193
  ```
@@ -185,7 +196,7 @@ Four organs, one loop:
185
196
 
186
197
  | Organ | Role |
187
198
  |---|---|
188
- | **Architect** | Reads the last trace + judge scores + history, credit-assigns the rubric loss to a node/route, proposes **one** change. Forgets nothing — skips mutations already tried-and-rejected. |
199
+ | **Architect** | Reads the last trace + judge scores + history, credit-assigns the rubric loss to a node/route, proposes **one** change. Forgets nothing: it skips mutations already tried and rejected. |
189
200
  | **SuiteRunner** | Runs each candidate against the held-out suite `R` times (repeats absorb judge noise). The only component that invokes the host MAS. |
190
201
  | **Judge** | LLM-as-judge: scores each run per a versioned rubric (`grounding`, `correctness`, `completeness`, …), with a per-step breakdown for credit assignment. |
191
202
  | **Gatekeeper** | Decides by margin `τ` + scope: auto-promote small wins, queue structural wins for a human, discard regressions, rollback if a later measurement regresses. |
@@ -194,37 +205,39 @@ Four organs, one loop:
194
205
 
195
206
  ## Quickstart
196
207
 
197
- ArchForge imports with **zero LLM** installed — adapters are import-lazy and self-skip when a provider SDK is absent. A real run needs one provider SDK.
208
+ ArchForge imports with **zero LLM** installed. The one provider client (LiteLLM) is import-lazy, and a real run needs LiteLLM (a core dep) plus the provider SDK(s) you actually run.
198
209
 
199
210
  ```bash
200
- # 1. Install from PyPI
211
+ # 1. Install from PyPI (LiteLLM ships as a core dependency; the provider SDKs it
212
+ # shells out to are optional extras)
201
213
  pip install archforge-optimizer
202
214
 
203
215
  # Optional: install the provider SDK(s) you actually run (none required to import)
204
216
  pip install "archforge-optimizer[providers-groq,providers-gemini]"
205
217
 
206
- # 2. Scaffold per-project config + the adapter package — writes:
207
- # .archforge/archforge.py (tunables — ACTIVE sane defaults)
218
+ # 2. Scaffold per-project config + the adapter package. This writes:
219
+ # .archforge/archforge.py (tunables, with sane defaults active)
208
220
  # .archforge/suite.json (the eval tasks you optimize against)
209
- # archforge_optimizer/ (a generic LangGraph adapter skeleton — 5 files)
221
+ # archforge_optimizer/ (a generic LangGraph adapter skeleton, 5 files)
210
222
  # __init__.py host.py app.py sidecar.py test_smoke_offline.py
211
223
  archforge-optimizer init
212
224
 
213
- # 3. Put your provider API key in a root `.env` (gitignored) ── e.g. GEMINI_API_KEY=...
214
- # (init never writes or touches .env — it just tells you to put the key there.)
225
+ # 3. Put your provider API key in a root `.env` (gitignored), e.g. GEMINI_API_KEY=...
226
+ # (init never writes or touches .env; it just tells you to put the key there.)
215
227
 
216
- # 4. Edit your MAS details into archforge_optimizer/app.py — fill every `# EDIT:` marker
217
- # (the node roster, edges, knobs, summarize/apply_llm_config hooks). Then build the
218
- # bootstrap Spec from your EDITED adapter: it lints the roster first and writes
219
- # archforge_optimizer/spec.json only if valid (rc=1 + the faults if not — fix + rerun).
220
- archforge-optimizer make-spec # → archforge_optimizer/spec.json (lint OK)
228
+ # 4. Edit your MAS details into archforge_optimizer/app.py. Fill every `# EDIT:`
229
+ # marker (the node roster, edges, knobs, summarize/apply_llm_config hooks). Then
230
+ # build the bootstrap Spec from your EDITED adapter: it lints the roster first and
231
+ # writes archforge_optimizer/spec.json only if valid (rc=1 + the faults if not,
232
+ # so fix and rerun).
233
+ archforge-optimizer make-spec # -> archforge_optimizer/spec.json (lint OK)
221
234
 
222
235
  # 5. Run one Propose-Evaluate-Commit cycle against your MAS. evolve auto-defaults
223
236
  # --adapter archforge_optimizer.host:AppAdapter and --seed archforge_optimizer/spec.json
224
237
  archforge-optimizer evolve
225
238
 
226
- # 6. Run the full loop: repeat evolve until K consecutive non-promotions (plateau)
227
- # or set flags in archforge.py
239
+ # 6. Run the full loop: repeat evolve until K consecutive non-promotions (plateau),
240
+ # or set flags in archforge.py
228
241
  archforge-optimizer evolve-loop --max-cycles 50
229
242
 
230
243
  # 7. Inspect
@@ -233,18 +246,19 @@ archforge-optimizer report # print per-attempt score deltas (incumbent vs ca
233
246
  archforge-optimizer approve --all # move PENDING_HUMAN structural wins into active
234
247
  ```
235
248
 
236
- > `init` never writes `.env.example` or `spec.json` — the provider key lives in your root
237
- > `.env` (gitignored), and `spec.json` comes from `make-spec` (your real roster, linted),
238
- > not a template. Lint any Spec by hand with `archforge-optimizer lint path/to/spec.json`.
249
+ The `--provider` flag selects the LLM backing the Architect + Judge (`anthropic` / `openai` / `groq` / `gemini` for real runs). Every real provider goes through **one LiteLLM client**: the provider just prefixes the model id (`openai/gpt-4o`, `gemini/gemini-3.6-flash`, …). The host MAS is wired via `--adapter my_pkg.my_host:MyAdapter`. After `init`, `evolve` already defaults it to `archforge_optimizer.host:AppAdapter`, so you only pass the flag for a custom adapter.
239
250
 
240
- > You can also invoke as `python -m archforge ...` — identical surface.
241
- >
242
- > **From source (development).** Clone the repo and `pip install -e .` for an editable install.
251
+ ### Evaluation backends
243
252
 
244
- The `--provider` flag selects the LLM backing the Architect + Judge (`anthropic` / `openai` / `groq` / `gemini` for real runs). The host MAS is wired via `--adapter my_pkg.my_host:MyAdapter` — and after `init`, `evolve` already defaults it to the `archforge_optimizer.host:AppAdapter`, so you only pass the flag for a custom adapter.
253
+ The Judge is pluggable behind one `JudgeProtocol` seam (`score` / `score_suite`), so the optimizer never knows which evaluator produced a score. `--evaluator` picks the backend (default `DEFAULT_EVALUATOR="native"` in `.archforge/archforge.py`):
254
+
255
+ - **`native`** (default): the built-in LLM-as-judge. One structured call to your `--provider` model scores every rubric dimension plus a per-step breakdown (used for credit assignment).
256
+ - **`deepeval`**: the external [DeepEval](https://deepeval.com) backend (optional: `pip install "archforge-optimizer[deepeval]"`). Each run is projected into a DeepEval `LLMTestCase` and scored by standalone metrics (`--deepeval-metric answer_relevancy --deepeval-metric faithfulness`, or the `DEFAULT_DEEPEVAL_METRICS` tunable). Metric scores land in `RunScore.rubric_scores`; the aggregate is their mean, comparable to the native judge's [0,1] aggregate. DeepEval is run-level, not per-step, so `step_scores` is empty and credit assignment degrades gracefully (the Architect falls back to conservative, blame-free proposals). The judge model is configurable: it reuses the `--judge-model` / `DEFAULT_JUDGE_MODELS` seam, prefixed with your `--provider` (e.g. `gemini/gemini-3.6-flash`) and routed through LiteLLM, so it scores with the same vendor and env keys as the rest of the run, never DeepEval's OpenAI default.
245
257
 
246
258
  ---
247
259
 
260
+
261
+
248
262
  ## The optimization loop (P-E-C)
249
263
 
250
264
  One cycle, end-to-end:
@@ -261,25 +275,25 @@ One cycle, end-to-end:
261
275
 
262
276
  ### The action space
263
277
 
264
- `ChangeKind` ∈ `prompt_edit | knob | add_node | remove_node | rewire | model_swap`. Scope is mechanical: `small` (prompt/knob) auto-promotes; `structural` (roster/graph/model) requires a human. A Spec Linter validates every candidate *before* it reaches the SuiteRunner — orphans, dangling refs, self-loops, type rules.
278
+ `ChangeKind` ∈ `prompt_edit | knob | add_node | remove_node | rewire | model_swap`. Scope is mechanical: `small` (prompt/knob) auto-promotes; `structural` (roster/graph/model) requires a human. A Spec Linter validates every candidate *before* it reaches the SuiteRunner (orphans, dangling refs, self-loops, type rules).
265
279
 
266
280
  ### Governing invariants
267
281
 
268
- - **I1** — `SpecStore.active()` is the only Spec any host run can instantiate.
269
- - **I2** — no committed Spec ever changes after `commit`.
270
- - **I3** — every non-root Spec has a reachable `parent_spec_id` chain; rollback preserves it.
271
- - **I4** — every structural win goes to `queue_for_human`; auto-promote never bypasses.
272
- - **I5** — no `Attempt.suite_result` ever compares scores across a different `rubric_id` or task set.
282
+ - **I1:** `SpecStore.active()` is the only Spec any host run can instantiate.
283
+ - **I2:** no committed Spec ever changes after `commit`.
284
+ - **I3:** every non-root Spec has a reachable `parent_spec_id` chain; rollback preserves it.
285
+ - **I4:** every structural win goes to `queue_for_human`; auto-promote never bypasses.
286
+ - **I5:** no `Attempt.suite_result` ever compares scores across a different `rubric_id` or task set.
273
287
 
274
288
  ### Error handling, by design
275
289
 
276
- Every failure that touches the lineage **fails closed** — the incumbent is untouched, the candidate discarded or held, traces retained. Noise is absorbed by `R` repeats + margin `τ` + regression floor `δ ≥ τ` (so a noisy measurement never yo-yos the pointer). Host/agent errors mid-run are caught per-task (`Trace.ok=false`, partial trace retained); a candidate that fails > ε of tasks is auto-rejected *before* margin math.
290
+ Every failure that touches the lineage **fails closed**: the incumbent is untouched, the candidate discarded or held, traces retained. Noise is absorbed by `R` repeats + margin `τ` + regression floor `δ ≥ τ` (so a noisy measurement never yo-yos the pointer). Host/agent errors mid-run are caught per-task (`Trace.ok=false`, partial trace retained); a candidate that fails > ε of tasks is auto-rejected *before* margin math.
277
291
 
278
292
  ---
279
293
 
280
294
  ## Adapters: connecting your MAS
281
295
 
282
- ArchForge couples to a host through one protocol — `HostMAS`:
296
+ ArchForge couples to a host through one protocol, `HostMAS`:
283
297
 
284
298
  ```python
285
299
  class HostMAS(Protocol):
@@ -288,11 +302,11 @@ class HostMAS(Protocol):
288
302
 
289
303
  Your adapter builds a runnable pipeline from `spec` (the active incumbent's nodes/edges/prompts/knobs) and threads `TracingMiddleware` through it so every step is recorded. Everything below the seam is your pipeline; everything above it is the Forge.
290
304
 
291
- A **generic LangGraph adapter** ships in `archforge/host/adapters/langgraph.py` and drives a real `graph.stream(...)` — "describe, don't introspect" (it reads node *names*, the stable surface; it never climbs your graph's internals). It is the easiest path for any LangGraph-based MAS. For other frameworks (CrewAI, AutoGen, raw call loops), subclass `BaseHostAdapter` (`archforge/host/adapters/base.py`) — the kit is factored so adapting *any* MAS is cheap, not bespoke-per-framework.
305
+ A **generic LangGraph adapter** ships in `archforge/host/adapters/langgraph.py` and drives a real `graph.stream(...)`: it "describes, doesn't introspect" (it reads node *names*, the stable surface; it never climbs your graph's internals). It is the easiest path for any LangGraph-based MAS. For other frameworks (CrewAI, AutoGen, raw call loops), subclass `BaseHostAdapter` (`archforge/host/adapters/base.py`). The kit is factored so adapting *any* MAS is cheap, not bespoke-per-framework.
292
306
 
293
- **`init` scaffolds the adapter for you.** You don't code the wiring from scratch: `archforge-optimizer init` writes a generic, name-neutral `archforge_optimizer/` package (the LangGraph adapter skeleton above) into your project root. Edit the `# EDIT:` markers in `archforge_optimizer/app.py` to describe your MAS — the node roster (`_NODES`), edges (`_EDGES`), knob to state map, and the `summarize`/`apply_llm_config`/`reset_llm_config` hooks — then `archforge-optimizer make-spec` builds + lints `archforge_optimizer/spec.json` from it. Once scaffolded, `evolve` auto-defaults to the scaffold: `--adapter archforge_optimizer.host:AppAdapter` and `--seed archforge_optimizer/spec.json` (only pass the flags for a custom adapter/seed). Per-file clobber guards mean re-running `init` never overwrites your edits unless `--force`, and a missing/half-edited adapter is repaired even when `archforge.py` already exists.
307
+ **`init` scaffolds the adapter for you.** You don't code the wiring from scratch. `archforge-optimizer init` writes a generic, name-neutral `archforge_optimizer/` package (the LangGraph adapter skeleton above) into your project root. Edit the `# EDIT:` markers in `archforge_optimizer/app.py` to describe your MAS (the node roster `_NODES`, edges `_EDGES`, knob to state map, and the `summarize` / `apply_llm_config` / `reset_llm_config` hooks). Then `archforge-optimizer make-spec` builds and lints `archforge_optimizer/spec.json` from it. Once scaffolded, `evolve` auto-defaults to the scaffold: `--adapter archforge_optimizer.host:AppAdapter` and `--seed archforge_optimizer/spec.json` (pass the flags only for a custom adapter/seed). Per-file clobber guards mean re-running `init` never overwrites your edits unless `--force`, and a missing/half-edited adapter is repaired even when `archforge.py` already exists.
294
308
 
295
- Run it via the dotted-path seam — the scaffolded package uses the same `module:Class` form:
309
+ Run it via the dotted-path seam (the scaffolded package uses the same `module:Class` form):
296
310
 
297
311
  ```bash
298
312
  archforge-optimizer evolve-loop # defaults: --adapter archforge_optimizer.host:AppAdapter --seed archforge_optimizer/spec.json
@@ -324,6 +338,8 @@ archforge-optimizer <command> [flags]
324
338
  | `--seed <path>` | bootstrap the root incumbent from a Spec JSON (first run); defaults to `archforge_optimizer/spec.json` when present |
325
339
  | `--adapter <dotted.path[:Class]>` | your `HostMAS` adapter; defaults to `archforge_optimizer.host:AppAdapter` when the scaffold is present (not on the `--provider scripted` fake path) |
326
340
  | `--provider {scripted\|anthropic\|openai\|groq\|gemini}` | LLM backing the Architect + Judge |
341
+ | `--evaluator {native\|deepeval}` | evaluation backend (default: `DEFAULT_EVALUATOR`); `deepeval` needs the `[deepeval]` extra |
342
+ | `--deepeval-metric {answer_relevancy\|faithfulness}` | DeepEval metric to score with (repeatable; default: `DEFAULT_DEEPEVAL_METRICS`) |
327
343
  | `--suite <path>` | evaluation suite JSON (default: `.archforge/suite.json`) |
328
344
  | `--tau <float>` | promotion margin τ |
329
345
  | `--delta <float>` | regression floor δ (≥ τ) |
@@ -342,29 +358,29 @@ archforge-optimizer <command> [flags]
342
358
 
343
359
  ## Configuration
344
360
 
345
- Per-project config lives in **`.archforge/archforge.py`** — a plain Python file, **active as-is** (no registration step), so `archforge-optimizer init` produces a working project directory immediately. Edit a value to change a default. `init` scaffolds it with sane defaults: `PROVIDER="gemini"`, `DEFAULT_TAU=0.05`, `DEFAULT_DELTA=0.07`, `DEFAULT_REPEATS=1`, `DEFAULT_MAX_CYCLES=20`, `DEFAULT_PLATEAU_CYCLES=5`, plus the budget caps, the architect model roster, and `DEFAULT_TRACE_TOTAL_BUDGET_TOK=None` (the tracing toggle — see below).
361
+ Per-project config lives in **`.archforge/archforge.py`**, a plain Python file that is **active as-is** (no registration step), so `archforge-optimizer init` produces a working project directory immediately. Edit a value to change a default. `init` scaffolds it with sane defaults: `PROVIDER="gemini"`, `DEFAULT_TAU=0.05`, `DEFAULT_DELTA=0.07`, `DEFAULT_REPEATS=1`, `DEFAULT_MAX_CYCLES=20`, `DEFAULT_PLATEAU_CYCLES=5`, plus the budget caps, the architect model roster, and `DEFAULT_TRACE_TOTAL_BUDGET_TOK=None` (the tracing toggle, see below).
346
362
 
347
- API keys live in **`.env`** (gitignored — your own keys, never logged or committed). `evolve` loads them from `.env` for `--provider != scripted`; the environment always wins, and `--api-key` wins above both.
363
+ API keys live in **`.env`**. `evolve` loads them for `--provider != scripted`; the environment always preferred
348
364
 
349
- The evaluation suite is **`.archforge/suite.json`** — the representative tasks the Judge scores. Optimization targets the *suite*, never a single repeated task (the primary defense against overfitting structural mutations).
365
+ The evaluation suite is **`.archforge/suite.json`**, the representative tasks the Judge scores. Optimization targets the *suite*, never a single repeated task (the primary defense against overfitting structural mutations).
350
366
 
351
367
  ---
352
368
 
353
369
  ## Observability (OpenTelemetry GenAI tracing)
354
370
 
355
- By default, each `Step` the Judge reads carries a host-authored one-liner summary (e.g. `answer_len=1189`) — lossy on the **host-streaming path**. ArchForge can instead auto-instrument your SDK calls as **OpenTelemetry GenAI spans** and project a **bounded slice** of the real prompt/completion into each `Step` — so the Judge compares real content against the task, not length stubs.
371
+ By default, each `Step` the Judge reads carries a host-authored one-liner summary (e.g. `answer_len=1189`), lossy on the **host-streaming path**. ArchForge can instead auto-instrument your SDK calls as **OpenTelemetry GenAI spans** and project a **bounded slice** of the real prompt/completion into each `Step`, so the Judge compares real content against the task, not length stubs.
356
372
 
357
- - **Cooperative attribution.** A forge-owned `wrapped(name, fn)` opens an `archforge.node` parent span; auto-instrumented LLM/retriever spans nest as children by parent-link (not temporal order) — robust to retries, multi-call, and fan-out.
373
+ - **Cooperative attribution.** A forge-owned `wrapped(name, fn)` opens an `archforge.node` parent span; auto-instrumented LLM/retriever spans nest as children by parent-link (not temporal order), robust to retries, multi-call, and fan-out.
358
374
  - **Bounded.** Per-kind caps keep the total judge-prompt token budget bounded; a post-loop shed trims the largest remaining steps while **protecting the final-answer step**.
359
375
  - **Gated, not forked.** `DEFAULT_TRACE_TOTAL_BUDGET_TOK = None` reproduces the lossy `summarize()` path **byte-identically**, so turning rich tracing off yields exactly the same `Step` records the Judge would read without OTel installed. Set a number to turn on rich steps. Toggle, not fork.
360
- - **Zero-dep by default.** `archforge.otel` is import-lazy — `import archforge` and `import archforge.otel` pull **zero** OpenTelemetry. Per-SDK instrumentors (`opentelemetry-instrumentation-<sdk>`) are the MAS owner's install.
361
- - **Secrets stay in-process.** The in-memory span buffer has no exporter — nothing leaves the process. Never wire an OTLP exporter without a redaction processor.
376
+ - **Zero-dep by default.** `archforge.otel` is import-lazy: `import archforge` and `import archforge.otel` pull **zero** OpenTelemetry. Per-SDK instrumentors (`opentelemetry-instrumentation-<sdk>`) are the MAS owner's install.
377
+ - **Secrets stay in-process.** The in-memory span buffer has no exporter, so nothing leaves the process. Never wire an OTLP exporter without a redaction processor.
362
378
 
363
379
  ---
364
380
 
365
381
  ## Deployment: shipping optimizations to production
366
382
 
367
- When a candidate auto-promotes, ArchForge can emit a **deploy envelope** — a self-contained JSON with the promoted Spec, the knobs to overlay, the scores, and the decision (margin + rule). Your MAS reads it at startup and applies the knobs without the Forge on the hot path. Opt-in via the engine's `on_deploy` hook (the CLI wires it to write `.archforge/optimized.json`); `on_cycle` is the richer per-cycle surface (specs, runs, change) for custom rendering/telemetry.
383
+ When a candidate auto-promotes, ArchForge can emit a **deploy envelope**: a self-contained JSON with the promoted Spec, the knobs to overlay, the scores, and the decision (margin + rule). Your MAS reads it at startup and applies the knobs without the Forge on the hot path. Opt in via the engine's `on_deploy` hook (the CLI wires it to write `.archforge/optimized.json`); `on_cycle` is the richer per-cycle surface (specs, runs, change) for custom rendering/telemetry.
368
384
 
369
385
  ---
370
386
 
@@ -372,16 +388,16 @@ When a candidate auto-promotes, ArchForge can emit a **deploy envelope** — a s
372
388
 
373
389
  ```
374
390
  archforge/
375
- cli.py the Forge — argparse entrypoint + per-command wiring
391
+ cli.py the Forge: argparse entrypoint + per-command wiring
376
392
  engine.py the P-E-C orchestrator + loop (E3/E8 budget/plateau)
377
393
  architect.py proposes one change per cycle (credit assignment, dedup)
378
- suite.py SuiteRunner — runs the eval suite R repeats
394
+ suite.py SuiteRunner: runs the eval suite R repeats
379
395
  judge/ LLM-as-judge (base.py + scripted.py)
380
396
  gatekeeper.py decides promote / queue / discard / rollback
381
397
  stores/ SpecStore (versioned) + TraceStore + AttemptStore (append-only)
382
- middleware.py TracingMiddleware — the host seam
398
+ middleware.py TracingMiddleware: the host seam
383
399
  host/ HostMAS protocol + adapter kit (base.py, langgraph.py, ...)
384
- llm/ provider clients (anthropic/openai/groq/gemini, lazy + self-skip)
400
+ llm/ one LiteLLM client for every real provider (import-lazy; provider = model prefix)
385
401
  otel.py OpenTelemetry GenAI tracing (import-lazy, bounded projection)
386
402
  lint.py Spec Linter (validate-DAG, refs, type rules)
387
403
  mutate.py apply a Change to a Spec
@@ -396,10 +412,10 @@ archforge/
396
412
  ## Extending ArchForge
397
413
 
398
414
  - **A new MAS.** Subclass `BaseHostAdapter` (or use the LangGraph adapter if you're on LangGraph), implement `instantiate(spec, middleware) -> Runnable`, and pass it via `--adapter`.
399
- - **A new provider.** Add a client under `archforge/llm/` (subclass `LLMClient`); register it in the CLI's `_PROVIDERS`.
415
+ - **A new provider.** Add a provider→prefix entry to `_PROVIDER_PREFIX` in `archforge/llm/litellm.py` and a model default in `.archforge/archforge.py`'s `DEFAULT_ARCHITECT_MODELS`. LiteLLM routes the prefixed model id for you. No new adapter.
400
416
  - **A new mutation kind.** Add it to `ChangeKind` + `scope_for_kind`, implement it in `mutate.apply_change`, and teach the Architect to propose it.
401
- - **A richer rubric.** Write a `suite.json` + rubric; the Judge scores each run against it. Comparisons are only valid within `(rubric_id, suite_id)` — bumping either starts a fresh baseline (I5).
402
- - **Custom cycle/deploy surfaces.** Pass callbacks into the `Engine` constructor: `on_cycle(result, ctx)` fires every cycle (the CLI uses it to print the per-cycle card; `ctx` carries the parent + candidate Specs, both `SuiteRun`s, and the proposed `Change`), and `on_deploy(spec, dctx)` fires only on `AUTO_PROMOTE` (the CLI uses it to write the `optimized.json` deploy envelope; `dctx` carries the parent Spec, the `Decision` with margin + rule, both runs' scores, and the cycle index).
417
+ - **A richer rubric.** Write a `suite.json` + rubric; the Judge scores each run against it. Comparisons are only valid within `(rubric_id, suite_id)`: bumping either starts a fresh baseline (I5).
418
+ - **Custom cycle/deploy surfaces.** Pass callbacks into the `Engine` constructor. `on_cycle(result, ctx)` fires every cycle (the CLI uses it to print the per-cycle card; `ctx` carries the parent + candidate Specs, both `SuiteRun`s, and the proposed `Change`). `on_deploy(spec, dctx)` fires only on `AUTO_PROMOTE` (the CLI uses it to write the `optimized.json` deploy envelope; `dctx` carries the parent Spec, the `Decision` with margin + rule, both runs' scores, and the cycle index).
403
419
 
404
420
  The public model surface (`archforge.models`) is the stable contract: `Spec`, `Node`, `Edge`, `Knobs`, `Step`, `Trace`, `RunScore`, `Attempt`, `Change`, `Thresholds`, and the `ChangeKind`/`Scope`/`Verdict`/`SpecStatus` enums. `archforge.host.base` defines `Task`, `AgentResponse`, `Agent`, `Runnable`, `HostMAS`.
405
421
 
@@ -408,8 +424,8 @@ The public model surface (`archforge.models`) is the stable contract: `Spec`, `N
408
424
  ## Requirements
409
425
 
410
426
  - Python ≥ 3.11 (developed on 3.14)
411
- - `pydantic >= 2.7`, `python-dotenv >= 1.0` (only hard deps — ArchForge imports cleanly with nothing else)
412
- - Provider SDKs (optional, install only what you run): `anthropic`, `openai`, `groq`, `google-genai`
427
+ - `pydantic >= 2.7`, `python-dotenv >= 1.0`, `litellm` (hard deps: LiteLLM is import-lazy, so ArchForge still imports cleanly with nothing else; it is only needed at a real provider call)
428
+ - Provider SDKs (optional, install only what you run; LiteLLM shells out to them): `anthropic`, `openai`, `groq`, `google-genai`
413
429
  - For rich tracing (optional): `opentelemetry-sdk` + the per-SDK instrumentors you call
414
430
 
415
431
  ---
@@ -419,22 +435,22 @@ The public model surface (`archforge.models`) is the stable contract: `Spec`, `N
419
435
  ArchForge is exercised end-to-end on a real LangGraph MAS (groq + google-genai + chroma, OpenTelemetry-traced). Active directions:
420
436
 
421
437
  - **Delegation specs** (replace hand-rolled subsystems with vetted libraries):
422
- - ✅ #1 — Tracing → OpenTelemetry GenAI (lands bounded real prompt/completion slices into the Judge's per-step `Step` records)
423
- - 🚧 #2 — LLM clients → LiteLLM (unify the per-provider clients behind one library)
424
- - 🚧 #3 — Judge → DeepEval / Ragas (rubric scoring via a mature eval framework)
438
+ - ✅ #1: Tracing → OpenTelemetry GenAI (lands bounded real prompt/completion slices into the Judge's per-step `Step` records)
439
+ - ✅ #2: LLM clients → LiteLLM (one client, provider = model prefix; #1)
440
+ - 🚧 #3: Judge → DeepEval / Ragas (rubric scoring via a mature eval framework)
425
441
  - **Adapter kit.** Generalize so adapting *any* MAS is cheap (LangGraph done; CrewAI/AutoGen/raw-loops next).
426
442
  - **Hierarchical search (v2).** A Strategist layer that emits scoped optimization goals, layered over the P-E-C loop once the cheap one-change loop is reliable.
427
443
 
428
- No part of the roadmap requires breaking the model surface — additions are additive and gated behind tunables.
444
+ No part of the roadmap requires breaking the model surface: additions are additive and gated behind tunables.
429
445
 
430
446
  ---
431
447
 
432
448
  ## License
433
449
 
434
- ArchForge is released under the **MIT License** — see [`LICENSE`](LICENSE) for the full text. © 2026 Vedant Pardeshi.
450
+ ArchForge is released under the **MIT License** (see [`LICENSE`](LICENSE) for the full text). © 2026 Vedant Pardeshi.
435
451
 
436
452
  ---
437
453
 
438
454
  <p align="center">
439
- *ArchForge never patches live state — it swaps which versioned pipeline the host uses.*
455
+ *ArchForge never patches live state. It swaps which versioned pipeline the host uses.*
440
456
  </p>