archforge-optimizer 0.2.0__tar.gz → 0.3.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.
- archforge_optimizer-0.3.0/.gitignore +6 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/PKG-INFO +79 -76
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/README.md +76 -74
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/config.py +1 -1
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/llm/__init__.py +15 -22
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/llm/base.py +30 -2
- archforge_optimizer-0.3.0/archforge/llm/litellm.py +141 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/pyproject.toml +5 -4
- archforge_optimizer-0.2.0/.gitignore +0 -12
- archforge_optimizer-0.2.0/archforge/llm/_common.py +0 -94
- archforge_optimizer-0.2.0/archforge/llm/anthropic.py +0 -90
- archforge_optimizer-0.2.0/archforge/llm/gemini.py +0 -112
- archforge_optimizer-0.2.0/archforge/llm/groq.py +0 -63
- archforge_optimizer-0.2.0/archforge/llm/openai.py +0 -63
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/LICENSE +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/__init__.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/__main__.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/architect.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/cli.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/config_init.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/diff.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/engine.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/gatekeeper.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/host/__init__.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/host/adapters/__init__.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/host/adapters/base.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/host/adapters/helpers.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/host/adapters/langgraph.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/host/base.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/host/fake.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/judge/__init__.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/judge/base.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/judge/scripted.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/lint.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/llm/scripted.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/middleware.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/models.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/mutate.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/otel.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/runlog.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/runner.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/spec_builder.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/stores/__init__.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/stores/_jsonl.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/stores/attempt_store.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/stores/spec_store.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/stores/trace_store.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/suite.py +0 -0
- {archforge_optimizer-0.2.0 → archforge_optimizer-0.3.0}/archforge/userconfig.py +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: archforge-optimizer
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: ArchForge
|
|
3
|
+
Version: 0.3.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
|
|
10
11
|
Provides-Extra: dev
|
|
@@ -40,20 +41,28 @@ Description-Content-Type: text/markdown
|
|
|
40
41
|
</p>
|
|
41
42
|
|
|
42
43
|
> A self-improving meta-layer over multi-agent systems.
|
|
43
|
-
> Point it at your graph, give it a rubric, and it evolves your pipeline
|
|
44
|
+
> Point it at your graph, give it a rubric, and it evolves your pipeline, one proven change per cycle.
|
|
44
45
|
|
|
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).
|
|
46
|
+
**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
47
|
|
|
47
|
-
|
|
48
|
+
## What is ArchForge?
|
|
49
|
+
|
|
50
|
+
ArchForge sits **on top** of an existing multi-agent system (MAS) and improves it over time. In plain terms:
|
|
51
|
+
|
|
52
|
+
1. **Look.** It inspects where the judge docked points on the last run.
|
|
53
|
+
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.
|
|
54
|
+
3. **Keep or drop.** It keeps the change only if it measurably beats the current pipeline on a held-out suite.
|
|
55
|
+
|
|
56
|
+
Your MAS keeps running tasks as normal. ArchForge watches those runs and feeds back an improved pipeline.
|
|
48
57
|
|
|
49
58
|
The design is deliberately minimal and verifiable:
|
|
50
59
|
|
|
51
|
-
- **One protected incumbent.** A candidate never touches production config
|
|
52
|
-
- **Immutable, versioned Specs are the single source of truth.** Evolving the pipeline
|
|
53
|
-
- **Hybrid autonomy.** Safe small edits (prompt/knob)
|
|
54
|
-
- **Observation/control asymmetry.** The wrapper records traces and reports the active Spec, but never rewrites prompts mid-run. All mutation happens
|
|
60
|
+
- **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 τ.
|
|
61
|
+
- **Immutable, versioned Specs are the single source of truth.** Evolving the pipeline means swapping which Spec the host instantiates, never patching live state.
|
|
62
|
+
- **Hybrid autonomy.** Safe small edits (prompt/knob) apply automatically. Structural edits (roster/graph/model) queue for human approval.
|
|
63
|
+
- **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
64
|
|
|
56
|
-
No ground truth is required
|
|
65
|
+
No ground truth is required. An LLM-as-judge scores each run against a rubric.
|
|
57
66
|
|
|
58
67
|
### At a glance
|
|
59
68
|
|
|
@@ -65,16 +74,16 @@ No ground truth is required — an LLM-as-judge scores each run against a rubric
|
|
|
65
74
|
| [**Judge**](archforge/judge/) | LLM-as-judge: scores each run per a versioned rubric, with a per-step breakdown for credit assignment |
|
|
66
75
|
| [**Gatekeeper**](archforge/gatekeeper.py) | Decides promote / queue-for-human / discard / rollback by margin `τ` + scope |
|
|
67
76
|
| [**Stores**](archforge/stores/) | `SpecStore` (versioned, content-addressed) + `TraceStore` + `AttemptStore` (all append-only) |
|
|
68
|
-
| [**TracingMiddleware**](archforge/middleware.py) | The host seam
|
|
77
|
+
| [**TracingMiddleware**](archforge/middleware.py) | The host seam: wraps every agent, records each `Step`, reports the active Spec |
|
|
69
78
|
|
|
70
79
|
---
|
|
71
80
|
|
|
72
81
|
## A Simple Example
|
|
73
82
|
|
|
74
|
-
One Propose-Evaluate-Commit cycle, **zero cost
|
|
83
|
+
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
84
|
|
|
76
85
|
```python
|
|
77
|
-
# save this as evolve_demo.py
|
|
86
|
+
# save this as evolve_demo.py, then run it from an init-ed project dir
|
|
78
87
|
import tempfile
|
|
79
88
|
from pathlib import Path
|
|
80
89
|
|
|
@@ -108,12 +117,12 @@ with tempfile.TemporaryDirectory() as d:
|
|
|
108
117
|
specs, atts, ts = SpecStore(root_dir), AttemptStore(root_dir), TraceStore(root_dir)
|
|
109
118
|
rid = specs.commit(seed, parent_spec_id=None, status=m.SpecStatus.INCUMBENT)
|
|
110
119
|
specs.set_active(rid)
|
|
111
|
-
# the candidate's spec_id is content-hashed OVER its parent
|
|
120
|
+
# the candidate's spec_id is content-hashed OVER its parent, so mirror the
|
|
112
121
|
# engine's commit (parent = rid) when scripting the judge's score for it
|
|
113
122
|
cand.parent_spec_id = rid
|
|
114
123
|
judge = (ScriptedJudge(rubric=rubric)
|
|
115
124
|
.set_aggregate(rid, "t1", 0.55) # incumbent baseline
|
|
116
|
-
.set_aggregate(cand.compute_spec_id(), "t1", 0.70)) # +0.15 >=
|
|
125
|
+
.set_aggregate(cand.compute_spec_id(), "t1", 0.70)) # +0.15 >= tau
|
|
117
126
|
|
|
118
127
|
engine = Engine(
|
|
119
128
|
host=FakeHostMAS(), judge=judge, architect=ScriptedArchitect().propose(change, {"prompt": "p1"}),
|
|
@@ -128,14 +137,14 @@ with tempfile.TemporaryDirectory() as d:
|
|
|
128
137
|
```
|
|
129
138
|
|
|
130
139
|
```
|
|
131
|
-
archforge-optimizer init # once
|
|
140
|
+
archforge-optimizer init # once: scaffolds project config + the archforge_optimizer/ adapter package
|
|
132
141
|
python evolve_demo.py
|
|
133
142
|
action=AUTO_PROMOTE margin=+0.15
|
|
134
143
|
incumbent_mean=0.55 candidate_mean=0.70
|
|
135
144
|
active_spec_id=57396a49 promoted=True
|
|
136
145
|
```
|
|
137
146
|
|
|
138
|
-
The candidate's tighter prompt beat the incumbent by `+0.15 ≥ τ`, so the Gatekeeper **auto-promoted** it to the new active Spec
|
|
147
|
+
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
148
|
|
|
140
149
|
---
|
|
141
150
|
|
|
@@ -176,7 +185,7 @@ The candidate's tighter prompt beat the incumbent by `+0.15 ≥ τ`, so the Gate
|
|
|
176
185
|
│ small + win → auto-promote │ structural + win → human gate │
|
|
177
186
|
│ lose → discard │ regress → rollback (lineage) │
|
|
178
187
|
│ ▼ │
|
|
179
|
-
│ SpecStore (versioned, immutable)
|
|
188
|
+
│ SpecStore (versioned, immutable) - "active incumbent" pointer │
|
|
180
189
|
└─────────────────────────────┬──────────────────────────────────┘
|
|
181
190
|
└── next host runs use the new incumbent Spec
|
|
182
191
|
```
|
|
@@ -185,7 +194,7 @@ Four organs, one loop:
|
|
|
185
194
|
|
|
186
195
|
| Organ | Role |
|
|
187
196
|
|---|---|
|
|
188
|
-
| **Architect** | Reads the last trace + judge scores + history, credit-assigns the rubric loss to a node/route, proposes **one** change. Forgets nothing
|
|
197
|
+
| **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
198
|
| **SuiteRunner** | Runs each candidate against the held-out suite `R` times (repeats absorb judge noise). The only component that invokes the host MAS. |
|
|
190
199
|
| **Judge** | LLM-as-judge: scores each run per a versioned rubric (`grounding`, `correctness`, `completeness`, …), with a per-step breakdown for credit assignment. |
|
|
191
200
|
| **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 +203,39 @@ Four organs, one loop:
|
|
|
194
203
|
|
|
195
204
|
## Quickstart
|
|
196
205
|
|
|
197
|
-
ArchForge imports with **zero LLM** installed
|
|
206
|
+
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
207
|
|
|
199
208
|
```bash
|
|
200
|
-
# 1. Install from PyPI
|
|
209
|
+
# 1. Install from PyPI (LiteLLM ships as a core dependency; the provider SDKs it
|
|
210
|
+
# shells out to are optional extras)
|
|
201
211
|
pip install archforge-optimizer
|
|
202
212
|
|
|
203
213
|
# Optional: install the provider SDK(s) you actually run (none required to import)
|
|
204
214
|
pip install "archforge-optimizer[providers-groq,providers-gemini]"
|
|
205
215
|
|
|
206
|
-
# 2. Scaffold per-project config + the adapter package
|
|
207
|
-
# .archforge/archforge.py (tunables
|
|
216
|
+
# 2. Scaffold per-project config + the adapter package. This writes:
|
|
217
|
+
# .archforge/archforge.py (tunables, with sane defaults active)
|
|
208
218
|
# .archforge/suite.json (the eval tasks you optimize against)
|
|
209
|
-
# archforge_optimizer/ (a generic LangGraph adapter skeleton
|
|
219
|
+
# archforge_optimizer/ (a generic LangGraph adapter skeleton, 5 files)
|
|
210
220
|
# __init__.py host.py app.py sidecar.py test_smoke_offline.py
|
|
211
221
|
archforge-optimizer init
|
|
212
222
|
|
|
213
|
-
# 3. Put your provider API key in a root `.env` (gitignored)
|
|
214
|
-
# (init never writes or touches .env
|
|
223
|
+
# 3. Put your provider API key in a root `.env` (gitignored), e.g. GEMINI_API_KEY=...
|
|
224
|
+
# (init never writes or touches .env; it just tells you to put the key there.)
|
|
215
225
|
|
|
216
|
-
# 4. Edit your MAS details into archforge_optimizer/app.py
|
|
217
|
-
# (the node roster, edges, knobs, summarize/apply_llm_config hooks). Then
|
|
218
|
-
# bootstrap Spec from your EDITED adapter: it lints the roster first and
|
|
219
|
-
# archforge_optimizer/spec.json only if valid (rc=1 + the faults if not
|
|
220
|
-
|
|
226
|
+
# 4. Edit your MAS details into archforge_optimizer/app.py. Fill every `# EDIT:`
|
|
227
|
+
# marker (the node roster, edges, knobs, summarize/apply_llm_config hooks). Then
|
|
228
|
+
# build the bootstrap Spec from your EDITED adapter: it lints the roster first and
|
|
229
|
+
# writes archforge_optimizer/spec.json only if valid (rc=1 + the faults if not,
|
|
230
|
+
# so fix and rerun).
|
|
231
|
+
archforge-optimizer make-spec # -> archforge_optimizer/spec.json (lint OK)
|
|
221
232
|
|
|
222
233
|
# 5. Run one Propose-Evaluate-Commit cycle against your MAS. evolve auto-defaults
|
|
223
234
|
# --adapter archforge_optimizer.host:AppAdapter and --seed archforge_optimizer/spec.json
|
|
224
235
|
archforge-optimizer evolve
|
|
225
236
|
|
|
226
|
-
# 6. Run the full loop: repeat evolve until K consecutive non-promotions (plateau)
|
|
227
|
-
#
|
|
237
|
+
# 6. Run the full loop: repeat evolve until K consecutive non-promotions (plateau),
|
|
238
|
+
# or set flags in archforge.py
|
|
228
239
|
archforge-optimizer evolve-loop --max-cycles 50
|
|
229
240
|
|
|
230
241
|
# 7. Inspect
|
|
@@ -233,15 +244,7 @@ archforge-optimizer report # print per-attempt score deltas (incumbent vs ca
|
|
|
233
244
|
archforge-optimizer approve --all # move PENDING_HUMAN structural wins into active
|
|
234
245
|
```
|
|
235
246
|
|
|
236
|
-
|
|
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`.
|
|
239
|
-
|
|
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.
|
|
243
|
-
|
|
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.
|
|
247
|
+
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.
|
|
245
248
|
|
|
246
249
|
---
|
|
247
250
|
|
|
@@ -261,25 +264,25 @@ One cycle, end-to-end:
|
|
|
261
264
|
|
|
262
265
|
### The action space
|
|
263
266
|
|
|
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
|
|
267
|
+
`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
268
|
|
|
266
269
|
### Governing invariants
|
|
267
270
|
|
|
268
|
-
- **I1
|
|
269
|
-
- **I2
|
|
270
|
-
- **I3
|
|
271
|
-
- **I4
|
|
272
|
-
- **I5
|
|
271
|
+
- **I1:** `SpecStore.active()` is the only Spec any host run can instantiate.
|
|
272
|
+
- **I2:** no committed Spec ever changes after `commit`.
|
|
273
|
+
- **I3:** every non-root Spec has a reachable `parent_spec_id` chain; rollback preserves it.
|
|
274
|
+
- **I4:** every structural win goes to `queue_for_human`; auto-promote never bypasses.
|
|
275
|
+
- **I5:** no `Attempt.suite_result` ever compares scores across a different `rubric_id` or task set.
|
|
273
276
|
|
|
274
277
|
### Error handling, by design
|
|
275
278
|
|
|
276
|
-
Every failure that touches the lineage **fails closed
|
|
279
|
+
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
280
|
|
|
278
281
|
---
|
|
279
282
|
|
|
280
283
|
## Adapters: connecting your MAS
|
|
281
284
|
|
|
282
|
-
ArchForge couples to a host through one protocol
|
|
285
|
+
ArchForge couples to a host through one protocol, `HostMAS`:
|
|
283
286
|
|
|
284
287
|
```python
|
|
285
288
|
class HostMAS(Protocol):
|
|
@@ -288,11 +291,11 @@ class HostMAS(Protocol):
|
|
|
288
291
|
|
|
289
292
|
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
293
|
|
|
291
|
-
A **generic LangGraph adapter** ships in `archforge/host/adapters/langgraph.py` and drives a real `graph.stream(...)
|
|
294
|
+
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
295
|
|
|
293
|
-
**`init` scaffolds the adapter for you.** You don't code the wiring from scratch
|
|
296
|
+
**`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
297
|
|
|
295
|
-
Run it via the dotted-path seam
|
|
298
|
+
Run it via the dotted-path seam (the scaffolded package uses the same `module:Class` form):
|
|
296
299
|
|
|
297
300
|
```bash
|
|
298
301
|
archforge-optimizer evolve-loop # defaults: --adapter archforge_optimizer.host:AppAdapter --seed archforge_optimizer/spec.json
|
|
@@ -342,29 +345,29 @@ archforge-optimizer <command> [flags]
|
|
|
342
345
|
|
|
343
346
|
## Configuration
|
|
344
347
|
|
|
345
|
-
Per-project config lives in **`.archforge/archforge.py
|
|
348
|
+
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
349
|
|
|
347
|
-
API keys live in **`.env
|
|
350
|
+
API keys live in **`.env`**. `evolve` loads them for `--provider != scripted`; the environment always preferred
|
|
348
351
|
|
|
349
|
-
The evaluation suite is **`.archforge/suite.json
|
|
352
|
+
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
353
|
|
|
351
354
|
---
|
|
352
355
|
|
|
353
356
|
## Observability (OpenTelemetry GenAI tracing)
|
|
354
357
|
|
|
355
|
-
By default, each `Step` the Judge reads carries a host-authored one-liner summary (e.g. `answer_len=1189`)
|
|
358
|
+
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
359
|
|
|
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)
|
|
360
|
+
- **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
361
|
- **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
362
|
- **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
|
|
361
|
-
- **Secrets stay in-process.** The in-memory span buffer has no exporter
|
|
363
|
+
- **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.
|
|
364
|
+
- **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
365
|
|
|
363
366
|
---
|
|
364
367
|
|
|
365
368
|
## Deployment: shipping optimizations to production
|
|
366
369
|
|
|
367
|
-
When a candidate auto-promotes, ArchForge can emit a **deploy envelope
|
|
370
|
+
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
371
|
|
|
369
372
|
---
|
|
370
373
|
|
|
@@ -372,16 +375,16 @@ When a candidate auto-promotes, ArchForge can emit a **deploy envelope** — a s
|
|
|
372
375
|
|
|
373
376
|
```
|
|
374
377
|
archforge/
|
|
375
|
-
cli.py the Forge
|
|
378
|
+
cli.py the Forge: argparse entrypoint + per-command wiring
|
|
376
379
|
engine.py the P-E-C orchestrator + loop (E3/E8 budget/plateau)
|
|
377
380
|
architect.py proposes one change per cycle (credit assignment, dedup)
|
|
378
|
-
suite.py SuiteRunner
|
|
381
|
+
suite.py SuiteRunner: runs the eval suite R repeats
|
|
379
382
|
judge/ LLM-as-judge (base.py + scripted.py)
|
|
380
383
|
gatekeeper.py decides promote / queue / discard / rollback
|
|
381
384
|
stores/ SpecStore (versioned) + TraceStore + AttemptStore (append-only)
|
|
382
|
-
middleware.py TracingMiddleware
|
|
385
|
+
middleware.py TracingMiddleware: the host seam
|
|
383
386
|
host/ HostMAS protocol + adapter kit (base.py, langgraph.py, ...)
|
|
384
|
-
llm/ provider
|
|
387
|
+
llm/ one LiteLLM client for every real provider (import-lazy; provider = model prefix)
|
|
385
388
|
otel.py OpenTelemetry GenAI tracing (import-lazy, bounded projection)
|
|
386
389
|
lint.py Spec Linter (validate-DAG, refs, type rules)
|
|
387
390
|
mutate.py apply a Change to a Spec
|
|
@@ -396,10 +399,10 @@ archforge/
|
|
|
396
399
|
## Extending ArchForge
|
|
397
400
|
|
|
398
401
|
- **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
|
|
402
|
+
- **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
403
|
- **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)
|
|
402
|
-
- **Custom cycle/deploy surfaces.** Pass callbacks into the `Engine` constructor
|
|
404
|
+
- **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).
|
|
405
|
+
- **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
406
|
|
|
404
407
|
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
408
|
|
|
@@ -408,8 +411,8 @@ The public model surface (`archforge.models`) is the stable contract: `Spec`, `N
|
|
|
408
411
|
## Requirements
|
|
409
412
|
|
|
410
413
|
- Python ≥ 3.11 (developed on 3.14)
|
|
411
|
-
- `pydantic >= 2.7`, `python-dotenv >= 1.0` (
|
|
412
|
-
- Provider SDKs (optional, install only what you run): `anthropic`, `openai`, `groq`, `google-genai`
|
|
414
|
+
- `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)
|
|
415
|
+
- Provider SDKs (optional, install only what you run; LiteLLM shells out to them): `anthropic`, `openai`, `groq`, `google-genai`
|
|
413
416
|
- For rich tracing (optional): `opentelemetry-sdk` + the per-SDK instrumentors you call
|
|
414
417
|
|
|
415
418
|
---
|
|
@@ -419,22 +422,22 @@ The public model surface (`archforge.models`) is the stable contract: `Spec`, `N
|
|
|
419
422
|
ArchForge is exercised end-to-end on a real LangGraph MAS (groq + google-genai + chroma, OpenTelemetry-traced). Active directions:
|
|
420
423
|
|
|
421
424
|
- **Delegation specs** (replace hand-rolled subsystems with vetted libraries):
|
|
422
|
-
- ✅ #1
|
|
423
|
-
-
|
|
424
|
-
- 🚧 #3
|
|
425
|
+
- ✅ #1: Tracing → OpenTelemetry GenAI (lands bounded real prompt/completion slices into the Judge's per-step `Step` records)
|
|
426
|
+
- ✅ #2: LLM clients → LiteLLM (one client, provider = model prefix; #1)
|
|
427
|
+
- 🚧 #3: Judge → DeepEval / Ragas (rubric scoring via a mature eval framework)
|
|
425
428
|
- **Adapter kit.** Generalize so adapting *any* MAS is cheap (LangGraph done; CrewAI/AutoGen/raw-loops next).
|
|
426
429
|
- **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
430
|
|
|
428
|
-
No part of the roadmap requires breaking the model surface
|
|
431
|
+
No part of the roadmap requires breaking the model surface: additions are additive and gated behind tunables.
|
|
429
432
|
|
|
430
433
|
---
|
|
431
434
|
|
|
432
435
|
## License
|
|
433
436
|
|
|
434
|
-
ArchForge is released under the **MIT License**
|
|
437
|
+
ArchForge is released under the **MIT License** (see [`LICENSE`](LICENSE) for the full text). © 2026 Vedant Pardeshi.
|
|
435
438
|
|
|
436
439
|
---
|
|
437
440
|
|
|
438
441
|
<p align="center">
|
|
439
|
-
*ArchForge never patches live state
|
|
442
|
+
*ArchForge never patches live state. It swaps which versioned pipeline the host uses.*
|
|
440
443
|
</p>
|