archforge-optimizer 0.1.0__tar.gz → 0.2.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.1.0 → archforge_optimizer-0.2.0}/.gitignore +1 -1
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/PKG-INFO +46 -26
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/README.md +45 -25
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/cli.py +198 -17
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/config.py +1 -1
- archforge_optimizer-0.2.0/archforge/config_init.py +592 -0
- archforge_optimizer-0.1.0/archforge/config_init.py +0 -150
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/LICENSE +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/__init__.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/__main__.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/architect.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/diff.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/engine.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/gatekeeper.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/__init__.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/adapters/__init__.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/adapters/base.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/adapters/helpers.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/adapters/langgraph.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/base.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/host/fake.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/judge/__init__.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/judge/base.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/judge/scripted.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/lint.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/__init__.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/_common.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/anthropic.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/base.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/gemini.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/groq.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/openai.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/llm/scripted.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/middleware.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/models.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/mutate.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/otel.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/runlog.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/runner.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/spec_builder.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/__init__.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/_jsonl.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/attempt_store.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/spec_store.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/stores/trace_store.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/suite.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/archforge/userconfig.py +0 -0
- {archforge_optimizer-0.1.0 → archforge_optimizer-0.2.0}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: archforge-optimizer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: ArchForge — a self-improving meta-layer over multi-agent systems
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -30,10 +30,19 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
|
|
31
31
|
# ArchForge
|
|
32
32
|
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img src="images/logo.png" width="160">
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white">
|
|
39
|
+
<img src="https://img.shields.io/badge/License-MIT-green.svg">
|
|
40
|
+
</p>
|
|
41
|
+
|
|
33
42
|
> A self-improving meta-layer over multi-agent systems.
|
|
34
43
|
> Point it at your graph, give it a rubric, and it evolves your pipeline — one proven change per cycle.
|
|
35
44
|
|
|
36
|
-
**
|
|
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).
|
|
37
46
|
|
|
38
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.
|
|
39
48
|
|
|
@@ -119,8 +128,8 @@ with tempfile.TemporaryDirectory() as d:
|
|
|
119
128
|
```
|
|
120
129
|
|
|
121
130
|
```
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
archforge-optimizer init # once — scaffolds project config + the archforge_optimizer/ adapter package
|
|
132
|
+
python evolve_demo.py
|
|
124
133
|
action=AUTO_PROMOTE margin=+0.15
|
|
125
134
|
incumbent_mean=0.55 candidate_mean=0.70
|
|
126
135
|
active_spec_id=57396a49 promoted=True
|
|
@@ -194,25 +203,29 @@ pip install archforge-optimizer
|
|
|
194
203
|
# Optional: install the provider SDK(s) you actually run (none required to import)
|
|
195
204
|
pip install "archforge-optimizer[providers-groq,providers-gemini]"
|
|
196
205
|
|
|
197
|
-
# 2. Scaffold per-project config — writes
|
|
198
|
-
#
|
|
206
|
+
# 2. Scaffold per-project config + the adapter package — writes:
|
|
207
|
+
# .archforge/archforge.py (tunables — ACTIVE sane defaults)
|
|
208
|
+
# .archforge/suite.json (the eval tasks you optimize against)
|
|
209
|
+
# archforge_optimizer/ (a generic LangGraph adapter skeleton — 5 files)
|
|
210
|
+
# __init__.py host.py app.py sidecar.py test_smoke_offline.py
|
|
199
211
|
archforge-optimizer init
|
|
200
212
|
|
|
201
|
-
# 3. Put your API
|
|
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.)
|
|
202
215
|
|
|
203
|
-
# 4.
|
|
204
|
-
|
|
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)
|
|
205
221
|
|
|
206
|
-
# 5. Run one Propose-Evaluate-Commit cycle against your MAS
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
--seed your_spec.json
|
|
222
|
+
# 5. Run one Propose-Evaluate-Commit cycle against your MAS. evolve auto-defaults
|
|
223
|
+
# --adapter archforge_optimizer.host:AppAdapter and --seed archforge_optimizer/spec.json
|
|
224
|
+
archforge-optimizer evolve
|
|
210
225
|
|
|
211
226
|
# 6. Run the full loop: repeat evolve until K consecutive non-promotions (plateau)
|
|
212
|
-
#
|
|
213
|
-
archforge-optimizer evolve-loop
|
|
214
|
-
--adapter your_pkg.your_host:YourAdapter \
|
|
215
|
-
--seed your_spec.json --max-cycles 50
|
|
227
|
+
# or set flags in archforge.py
|
|
228
|
+
archforge-optimizer evolve-loop --max-cycles 50
|
|
216
229
|
|
|
217
230
|
# 7. Inspect
|
|
218
231
|
archforge-optimizer status # print the active incumbent Spec id, lineage, counts
|
|
@@ -220,11 +233,15 @@ archforge-optimizer report # print per-attempt score deltas (incumbent vs ca
|
|
|
220
233
|
archforge-optimizer approve --all # move PENDING_HUMAN structural wins into active
|
|
221
234
|
```
|
|
222
235
|
|
|
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`.
|
|
239
|
+
|
|
223
240
|
> You can also invoke as `python -m archforge ...` — identical surface.
|
|
224
241
|
>
|
|
225
242
|
> **From source (development).** Clone the repo and `pip install -e .` for an editable install.
|
|
226
243
|
|
|
227
|
-
The `--provider` flag selects the LLM backing the Architect + Judge (`
|
|
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.
|
|
228
245
|
|
|
229
246
|
---
|
|
230
247
|
|
|
@@ -273,10 +290,12 @@ Your adapter builds a runnable pipeline from `spec` (the active incumbent's node
|
|
|
273
290
|
|
|
274
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.
|
|
275
292
|
|
|
276
|
-
|
|
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.
|
|
294
|
+
|
|
295
|
+
Run it via the dotted-path seam — the scaffolded package uses the same `module:Class` form:
|
|
277
296
|
|
|
278
297
|
```bash
|
|
279
|
-
archforge-optimizer evolve-loop --adapter
|
|
298
|
+
archforge-optimizer evolve-loop # defaults: --adapter archforge_optimizer.host:AppAdapter --seed archforge_optimizer/spec.json
|
|
280
299
|
```
|
|
281
300
|
|
|
282
301
|
---
|
|
@@ -286,7 +305,8 @@ archforge-optimizer evolve-loop --adapter your_pkg.your_host:YourAdapter --seed
|
|
|
286
305
|
```
|
|
287
306
|
archforge-optimizer <command> [flags]
|
|
288
307
|
|
|
289
|
-
init scaffold .archforge/archforge.py + .
|
|
308
|
+
init scaffold .archforge/archforge.py + suite.json + the archforge_optimizer/ adapter package
|
|
309
|
+
make-spec build + lint archforge_optimizer/spec.json from the EDITED adapter (writes only if it passes)
|
|
290
310
|
lint <path> run the Spec Linter on a JSON Spec file
|
|
291
311
|
evolve run one Propose-Evaluate-Commit cycle from the active incumbent
|
|
292
312
|
evolve-loop repeat evolve until the budget cap or a plateau
|
|
@@ -301,8 +321,8 @@ archforge-optimizer <command> [flags]
|
|
|
301
321
|
| Flag | Purpose |
|
|
302
322
|
|---|---|
|
|
303
323
|
| `--root <dir>` | project root holding `.archforge/` (default `.`) |
|
|
304
|
-
| `--seed <path>` | bootstrap the root incumbent from a Spec JSON (first run) |
|
|
305
|
-
| `--adapter <dotted.path[:Class]>` | your `HostMAS` adapter |
|
|
324
|
+
| `--seed <path>` | bootstrap the root incumbent from a Spec JSON (first run); defaults to `archforge_optimizer/spec.json` when present |
|
|
325
|
+
| `--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) |
|
|
306
326
|
| `--provider {scripted\|anthropic\|openai\|groq\|gemini}` | LLM backing the Architect + Judge |
|
|
307
327
|
| `--suite <path>` | evaluation suite JSON (default: `.archforge/suite.json`) |
|
|
308
328
|
| `--tau <float>` | promotion margin τ |
|
|
@@ -368,7 +388,7 @@ archforge/
|
|
|
368
388
|
diff.py spec_diff / format_diff (human-readable mutation deltas)
|
|
369
389
|
runlog.py per-cycle run log (cards)
|
|
370
390
|
models.py Spec/Node/Edge/Step/Trace/RunScore/Attempt/Change/...
|
|
371
|
-
config.py
|
|
391
|
+
config.py / userconfig.py / config_init.py versioning + tunable resolver + `init`
|
|
372
392
|
```
|
|
373
393
|
|
|
374
394
|
---
|
|
@@ -392,8 +412,6 @@ The public model surface (`archforge.models`) is the stable contract: `Spec`, `N
|
|
|
392
412
|
- Provider SDKs (optional, install only what you run): `anthropic`, `openai`, `groq`, `google-genai`
|
|
393
413
|
- For rich tracing (optional): `opentelemetry-sdk` + the per-SDK instrumentors you call
|
|
394
414
|
|
|
395
|
-
> **Installing from source (development).** For an editable install, `pip install -e .` from a clone of this repository. A flat `pip install .` makes a *non-editable* copy in site-packages, so any later source edit won't take effect at the CLI — if a repo edit ever seems to no-op, check `python -c "import archforge; print(archforge.__file__)"` resolves to the repo, not site-packages.
|
|
396
|
-
|
|
397
415
|
---
|
|
398
416
|
|
|
399
417
|
## Roadmap
|
|
@@ -417,4 +435,6 @@ ArchForge is released under the **MIT License** — see [`LICENSE`](LICENSE) for
|
|
|
417
435
|
|
|
418
436
|
---
|
|
419
437
|
|
|
438
|
+
<p align="center">
|
|
420
439
|
*ArchForge never patches live state — it swaps which versioned pipeline the host uses.*
|
|
440
|
+
</p>
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# ArchForge
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="images/logo.png" width="160">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white">
|
|
9
|
+
<img src="https://img.shields.io/badge/License-MIT-green.svg">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
3
12
|
> A self-improving meta-layer over multi-agent systems.
|
|
4
13
|
> Point it at your graph, give it a rubric, and it evolves your pipeline — one proven change per cycle.
|
|
5
14
|
|
|
6
|
-
**
|
|
15
|
+
**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).
|
|
7
16
|
|
|
8
17
|
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.
|
|
9
18
|
|
|
@@ -89,8 +98,8 @@ with tempfile.TemporaryDirectory() as d:
|
|
|
89
98
|
```
|
|
90
99
|
|
|
91
100
|
```
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
archforge-optimizer init # once — scaffolds project config + the archforge_optimizer/ adapter package
|
|
102
|
+
python evolve_demo.py
|
|
94
103
|
action=AUTO_PROMOTE margin=+0.15
|
|
95
104
|
incumbent_mean=0.55 candidate_mean=0.70
|
|
96
105
|
active_spec_id=57396a49 promoted=True
|
|
@@ -164,25 +173,29 @@ pip install archforge-optimizer
|
|
|
164
173
|
# Optional: install the provider SDK(s) you actually run (none required to import)
|
|
165
174
|
pip install "archforge-optimizer[providers-groq,providers-gemini]"
|
|
166
175
|
|
|
167
|
-
# 2. Scaffold per-project config — writes
|
|
168
|
-
#
|
|
176
|
+
# 2. Scaffold per-project config + the adapter package — writes:
|
|
177
|
+
# .archforge/archforge.py (tunables — ACTIVE sane defaults)
|
|
178
|
+
# .archforge/suite.json (the eval tasks you optimize against)
|
|
179
|
+
# archforge_optimizer/ (a generic LangGraph adapter skeleton — 5 files)
|
|
180
|
+
# __init__.py host.py app.py sidecar.py test_smoke_offline.py
|
|
169
181
|
archforge-optimizer init
|
|
170
182
|
|
|
171
|
-
# 3. Put your API
|
|
183
|
+
# 3. Put your provider API key in a root `.env` (gitignored) ── e.g. GEMINI_API_KEY=...
|
|
184
|
+
# (init never writes or touches .env — it just tells you to put the key there.)
|
|
172
185
|
|
|
173
|
-
# 4.
|
|
174
|
-
|
|
186
|
+
# 4. Edit your MAS details into archforge_optimizer/app.py — fill every `# EDIT:` marker
|
|
187
|
+
# (the node roster, edges, knobs, summarize/apply_llm_config hooks). Then build the
|
|
188
|
+
# bootstrap Spec from your EDITED adapter: it lints the roster first and writes
|
|
189
|
+
# archforge_optimizer/spec.json only if valid (rc=1 + the faults if not — fix + rerun).
|
|
190
|
+
archforge-optimizer make-spec # → archforge_optimizer/spec.json (lint OK)
|
|
175
191
|
|
|
176
|
-
# 5. Run one Propose-Evaluate-Commit cycle against your MAS
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
--seed your_spec.json
|
|
192
|
+
# 5. Run one Propose-Evaluate-Commit cycle against your MAS. evolve auto-defaults
|
|
193
|
+
# --adapter archforge_optimizer.host:AppAdapter and --seed archforge_optimizer/spec.json
|
|
194
|
+
archforge-optimizer evolve
|
|
180
195
|
|
|
181
196
|
# 6. Run the full loop: repeat evolve until K consecutive non-promotions (plateau)
|
|
182
|
-
#
|
|
183
|
-
archforge-optimizer evolve-loop
|
|
184
|
-
--adapter your_pkg.your_host:YourAdapter \
|
|
185
|
-
--seed your_spec.json --max-cycles 50
|
|
197
|
+
# or set flags in archforge.py
|
|
198
|
+
archforge-optimizer evolve-loop --max-cycles 50
|
|
186
199
|
|
|
187
200
|
# 7. Inspect
|
|
188
201
|
archforge-optimizer status # print the active incumbent Spec id, lineage, counts
|
|
@@ -190,11 +203,15 @@ archforge-optimizer report # print per-attempt score deltas (incumbent vs ca
|
|
|
190
203
|
archforge-optimizer approve --all # move PENDING_HUMAN structural wins into active
|
|
191
204
|
```
|
|
192
205
|
|
|
206
|
+
> `init` never writes `.env.example` or `spec.json` — the provider key lives in your root
|
|
207
|
+
> `.env` (gitignored), and `spec.json` comes from `make-spec` (your real roster, linted),
|
|
208
|
+
> not a template. Lint any Spec by hand with `archforge-optimizer lint path/to/spec.json`.
|
|
209
|
+
|
|
193
210
|
> You can also invoke as `python -m archforge ...` — identical surface.
|
|
194
211
|
>
|
|
195
212
|
> **From source (development).** Clone the repo and `pip install -e .` for an editable install.
|
|
196
213
|
|
|
197
|
-
The `--provider` flag selects the LLM backing the Architect + Judge (`
|
|
214
|
+
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.
|
|
198
215
|
|
|
199
216
|
---
|
|
200
217
|
|
|
@@ -243,10 +260,12 @@ Your adapter builds a runnable pipeline from `spec` (the active incumbent's node
|
|
|
243
260
|
|
|
244
261
|
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.
|
|
245
262
|
|
|
246
|
-
|
|
263
|
+
**`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.
|
|
264
|
+
|
|
265
|
+
Run it via the dotted-path seam — the scaffolded package uses the same `module:Class` form:
|
|
247
266
|
|
|
248
267
|
```bash
|
|
249
|
-
archforge-optimizer evolve-loop --adapter
|
|
268
|
+
archforge-optimizer evolve-loop # defaults: --adapter archforge_optimizer.host:AppAdapter --seed archforge_optimizer/spec.json
|
|
250
269
|
```
|
|
251
270
|
|
|
252
271
|
---
|
|
@@ -256,7 +275,8 @@ archforge-optimizer evolve-loop --adapter your_pkg.your_host:YourAdapter --seed
|
|
|
256
275
|
```
|
|
257
276
|
archforge-optimizer <command> [flags]
|
|
258
277
|
|
|
259
|
-
init scaffold .archforge/archforge.py + .
|
|
278
|
+
init scaffold .archforge/archforge.py + suite.json + the archforge_optimizer/ adapter package
|
|
279
|
+
make-spec build + lint archforge_optimizer/spec.json from the EDITED adapter (writes only if it passes)
|
|
260
280
|
lint <path> run the Spec Linter on a JSON Spec file
|
|
261
281
|
evolve run one Propose-Evaluate-Commit cycle from the active incumbent
|
|
262
282
|
evolve-loop repeat evolve until the budget cap or a plateau
|
|
@@ -271,8 +291,8 @@ archforge-optimizer <command> [flags]
|
|
|
271
291
|
| Flag | Purpose |
|
|
272
292
|
|---|---|
|
|
273
293
|
| `--root <dir>` | project root holding `.archforge/` (default `.`) |
|
|
274
|
-
| `--seed <path>` | bootstrap the root incumbent from a Spec JSON (first run) |
|
|
275
|
-
| `--adapter <dotted.path[:Class]>` | your `HostMAS` adapter |
|
|
294
|
+
| `--seed <path>` | bootstrap the root incumbent from a Spec JSON (first run); defaults to `archforge_optimizer/spec.json` when present |
|
|
295
|
+
| `--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) |
|
|
276
296
|
| `--provider {scripted\|anthropic\|openai\|groq\|gemini}` | LLM backing the Architect + Judge |
|
|
277
297
|
| `--suite <path>` | evaluation suite JSON (default: `.archforge/suite.json`) |
|
|
278
298
|
| `--tau <float>` | promotion margin τ |
|
|
@@ -338,7 +358,7 @@ archforge/
|
|
|
338
358
|
diff.py spec_diff / format_diff (human-readable mutation deltas)
|
|
339
359
|
runlog.py per-cycle run log (cards)
|
|
340
360
|
models.py Spec/Node/Edge/Step/Trace/RunScore/Attempt/Change/...
|
|
341
|
-
config.py
|
|
361
|
+
config.py / userconfig.py / config_init.py versioning + tunable resolver + `init`
|
|
342
362
|
```
|
|
343
363
|
|
|
344
364
|
---
|
|
@@ -362,8 +382,6 @@ The public model surface (`archforge.models`) is the stable contract: `Spec`, `N
|
|
|
362
382
|
- Provider SDKs (optional, install only what you run): `anthropic`, `openai`, `groq`, `google-genai`
|
|
363
383
|
- For rich tracing (optional): `opentelemetry-sdk` + the per-SDK instrumentors you call
|
|
364
384
|
|
|
365
|
-
> **Installing from source (development).** For an editable install, `pip install -e .` from a clone of this repository. A flat `pip install .` makes a *non-editable* copy in site-packages, so any later source edit won't take effect at the CLI — if a repo edit ever seems to no-op, check `python -c "import archforge; print(archforge.__file__)"` resolves to the repo, not site-packages.
|
|
366
|
-
|
|
367
385
|
---
|
|
368
386
|
|
|
369
387
|
## Roadmap
|
|
@@ -387,4 +405,6 @@ ArchForge is released under the **MIT License** — see [`LICENSE`](LICENSE) for
|
|
|
387
405
|
|
|
388
406
|
---
|
|
389
407
|
|
|
408
|
+
<p align="center">
|
|
390
409
|
*ArchForge never patches live state — it swaps which versioned pipeline the host uses.*
|
|
410
|
+
</p>
|
|
@@ -53,7 +53,9 @@ from archforge.lint import lint
|
|
|
53
53
|
from archforge.runlog import RunLog
|
|
54
54
|
from archforge.stores import AttemptStore, SpecStore, TraceStore
|
|
55
55
|
from archforge.suite import Suite, load_suite_file
|
|
56
|
-
from archforge.config_init import
|
|
56
|
+
from archforge.config_init import (
|
|
57
|
+
archforge_config_text, _DEFAULT_SUITE_JSON, adapter_package_files,
|
|
58
|
+
)
|
|
57
59
|
|
|
58
60
|
# System config (provider roster, CLI fixtures, PROG, .env loader) — single source
|
|
59
61
|
# in archforge.config. The TUNABLE defaults (tau/delta/repeats/provider/models/…)
|
|
@@ -230,12 +232,23 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
230
232
|
_add_store_args(lint_p)
|
|
231
233
|
lint_p.add_argument("path", help="path to a Spec JSON file")
|
|
232
234
|
|
|
233
|
-
# --- init (scaffold user config)
|
|
235
|
+
# --- init (scaffold user config + generic adapter package) ----------------
|
|
234
236
|
init_p = sub.add_parser("init",
|
|
235
|
-
help="scaffold .archforge/archforge.py +
|
|
237
|
+
help="scaffold .archforge/archforge.py + the archforge_optimizer/ adapter package")
|
|
236
238
|
_add_store_args(init_p) # --root selects where archforge.py is written
|
|
237
239
|
init_p.add_argument("--force", action="store_true",
|
|
238
|
-
help="overwrite an existing .archforge/archforge.py")
|
|
240
|
+
help="overwrite an existing .archforge/archforge.py + adapter package")
|
|
241
|
+
|
|
242
|
+
# --- make-spec (build spec.json from the edited adapter) ------------------
|
|
243
|
+
mk = sub.add_parser("make-spec",
|
|
244
|
+
help="build + lint archforge_optimizer/spec.json from the edited adapter app")
|
|
245
|
+
_add_store_args(mk)
|
|
246
|
+
mk.add_argument("--adapter", metavar="DOTTED.PATH[:Class]",
|
|
247
|
+
default="archforge_optimizer.host:AppAdapter",
|
|
248
|
+
help="adapter module:Class whose app.build_spec() builds the Spec "
|
|
249
|
+
"(default: archforge_optimizer.host:AppAdapter)")
|
|
250
|
+
mk.add_argument("--out", metavar="PATH", default="archforge_optimizer/spec.json",
|
|
251
|
+
help="where to write the Spec JSON (default: archforge_optimizer/spec.json)")
|
|
239
252
|
|
|
240
253
|
return parser
|
|
241
254
|
|
|
@@ -519,6 +532,23 @@ def _install_resource_warning_quieteners() -> None:
|
|
|
519
532
|
_install_resource_warning_quieteners()
|
|
520
533
|
|
|
521
534
|
|
|
535
|
+
# The scaffolded adapter package (`archforge-optimizer init`) + its make-spec output.
|
|
536
|
+
# `evolve` defaults to these when present (no `--adapter`/`--seed` flag needed); the
|
|
537
|
+
# flags stay for custom adapters / arbitrary seed paths.
|
|
538
|
+
_SCAFFOLD_ADAPTER = "archforge_optimizer.host:AppAdapter"
|
|
539
|
+
_SCAFFOLD_SPEC = "archforge_optimizer/spec.json"
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _scaffolded_adapter_available() -> bool:
|
|
543
|
+
"""True when `init` wrote the adapter package at cwd (its host.py exists)."""
|
|
544
|
+
return (Path.cwd() / "archforge_optimizer" / "host.py").is_file()
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _scaffolded_spec_available() -> bool:
|
|
548
|
+
"""True when `make-spec` produced archforge_optimizer/spec.json at cwd."""
|
|
549
|
+
return (Path.cwd() / "archforge_optimizer" / "spec.json").is_file()
|
|
550
|
+
|
|
551
|
+
|
|
522
552
|
def _cmd_evolve(args: argparse.Namespace, *, components: Components | None,
|
|
523
553
|
loop: bool) -> int:
|
|
524
554
|
# Injected `components` (the test/embedding path) always win — they ARE the
|
|
@@ -527,6 +557,7 @@ def _cmd_evolve(args: argparse.Namespace, *, components: Components | None,
|
|
|
527
557
|
# works" contract): a project with no .archforge/archforge.py gets the init hint
|
|
528
558
|
# and rc 1 instead of a bogus run. No-op under the pytest gate (tests use the
|
|
529
559
|
# in-memory sane template).
|
|
560
|
+
components_injected = components is not None
|
|
530
561
|
if components is None:
|
|
531
562
|
try:
|
|
532
563
|
ucfg.ensure_initialized()
|
|
@@ -544,6 +575,11 @@ def _cmd_evolve(args: argparse.Namespace, *, components: Components | None,
|
|
|
544
575
|
|
|
545
576
|
specs, atts, traces = _stores(args.root)
|
|
546
577
|
|
|
578
|
+
# Default `--seed` to the scaffolded spec.json when the user scaffolded +
|
|
579
|
+
# ran `make-spec` (no flag needed); the flag still overrides for custom seeds.
|
|
580
|
+
if getattr(args, "seed", None) is None and _scaffolded_spec_available():
|
|
581
|
+
args.seed = _SCAFFOLD_SPEC
|
|
582
|
+
|
|
547
583
|
# zero-LLM bootstrap of the root incumbent from --seed (if none active)
|
|
548
584
|
active = _ensure_incumbent(args, specs)
|
|
549
585
|
if active is None:
|
|
@@ -556,7 +592,15 @@ def _cmd_evolve(args: argparse.Namespace, *, components: Components | None,
|
|
|
556
592
|
# --adapter: swap the runtime host for an external MAS adapter (a HostMAS /
|
|
557
593
|
# BaseHostAdapter) while keeping the --provider organs (Architect/Judge).
|
|
558
594
|
# This is the "adapt any MAS" seam: point at an adapter class, no core edit.
|
|
595
|
+
# When the user scaffolded the adapter package (`init`), DEFAULT --adapter to
|
|
596
|
+
# its AppAdapter (no flag needed); the flag still overrides for custom adapters.
|
|
597
|
+
# Skipped on the scripted fake path (FakeHostMAS is the zero-cost host) and
|
|
598
|
+
# when `components` were injected (test/embedding path owns the host).
|
|
559
599
|
adapter_path = getattr(args, "adapter", None)
|
|
600
|
+
if (adapter_path is None and not components_injected
|
|
601
|
+
and _scaffolded_adapter_available()
|
|
602
|
+
and (args.provider or ucfg.get("PROVIDER")) != "scripted"):
|
|
603
|
+
adapter_path = _SCAFFOLD_ADAPTER
|
|
560
604
|
if adapter_path:
|
|
561
605
|
organs = Components(host=_import_adapter(adapter_path), judge=organs.judge,
|
|
562
606
|
architect=organs.architect, suite=organs.suite)
|
|
@@ -788,13 +832,48 @@ def _cmd_lint(path: str) -> int:
|
|
|
788
832
|
|
|
789
833
|
|
|
790
834
|
def _cmd_init(args: argparse.Namespace) -> int:
|
|
791
|
-
"""Scaffold
|
|
792
|
-
|
|
793
|
-
|
|
835
|
+
"""Scaffold the ArchForge project:
|
|
836
|
+
|
|
837
|
+
- ``archforge_optimizer/`` — a generic, name-neutral LangGraph adapter skeleton
|
|
838
|
+
at the project root (cwd) whose ``# EDIT:`` markers the user fills for their MAS.
|
|
839
|
+
- ``.archforge/archforge.py`` — user-editable ACTIVE config (sane defaults).
|
|
840
|
+
- ``.archforge/suite.json`` — one-task eval sidecar (the user tunes the tasks).
|
|
841
|
+
|
|
842
|
+
Does NOT write ``.env.example`` or ``spec.json``. The user's provider API key goes
|
|
843
|
+
in the repo-root ``.env`` (gitignored); the bootstrap Spec is generated by the
|
|
844
|
+
separate ``make-spec`` command from the EDITED adapter (not a placeholder template).
|
|
845
|
+
Never touches a real ``.env``; nothing is echoed. Refuses to clobber an existing
|
|
846
|
+
``archforge.py`` unless ``--force``; never overwrites ``suite.json``; repairs the
|
|
847
|
+
adapter per-file (only fills missing files unless ``--force``).
|
|
848
|
+
|
|
849
|
+
The adapter scaffold runs FIRST so a re-run with an existing ``archforge.py`` can
|
|
850
|
+
still restore/repair a missing or half-edited ``archforge_optimizer/`` without
|
|
851
|
+
``--force`` — the per-file keep/``--force`` guards are independent of ``.archforge/``.
|
|
852
|
+
The ``archforge.py`` refuse-clobber then fires its rc=2 only AFTER the adapter is
|
|
853
|
+
already in place, preserving the existing contract."""
|
|
794
854
|
root = Path(args.root or _DEFAULT_ROOT)
|
|
795
855
|
cfg_path = root / "archforge.py"
|
|
856
|
+
pkg_dir = Path.cwd() / "archforge_optimizer"
|
|
857
|
+
|
|
858
|
+
# 1. archforge_optimizer/ — the generic LangGraph adapter skeleton, scaffolded at
|
|
859
|
+
# the project root (cwd) so `--adapter archforge_optimizer.host:AppAdapter` resolves
|
|
860
|
+
# (main() puts cwd on sys.path). The user EDITS their MAS details (the # EDIT:
|
|
861
|
+
# markers in app.py) instead of coding the wiring from scratch. Per-file clobber
|
|
862
|
+
# guard mirroring suite.json: never overwrite an existing file unless --force, so a
|
|
863
|
+
# half-edited scaffold still gets its missing files filled. Runs FIRST (independent
|
|
864
|
+
# of .archforge/) so a re-run can repair a deleted adapter without --force.
|
|
865
|
+
for relpath, content in adapter_package_files().items():
|
|
866
|
+
fpath = pkg_dir / relpath
|
|
867
|
+
if fpath.exists() and not args.force:
|
|
868
|
+
print(f"kept: archforge_optimizer/{relpath} (already present)")
|
|
869
|
+
continue
|
|
870
|
+
fpath.parent.mkdir(parents=True, exist_ok=True)
|
|
871
|
+
fpath.write_text(content, encoding="utf-8")
|
|
872
|
+
print(f"created: archforge_optimizer/{relpath}")
|
|
796
873
|
|
|
797
|
-
#
|
|
874
|
+
# 2. archforge.py — refuse-clobber unless --force. (rc=2 if exists, not --force.)
|
|
875
|
+
# Runs AFTER the adapter scaffold so a re-run that only needs to repair
|
|
876
|
+
# archforge_optimizer/ still gets it even when archforge.py is already present.
|
|
798
877
|
if cfg_path.exists() and not args.force:
|
|
799
878
|
print(f"! {cfg_path} already exists. Re-run with --force to overwrite "
|
|
800
879
|
"(your edits would be lost).", file=sys.stderr)
|
|
@@ -803,14 +882,6 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|
|
803
882
|
cfg_path.write_text(archforge_config_text(), encoding="utf-8")
|
|
804
883
|
print(f"created: {cfg_path} (edit a value to change a default; the file is ACTIVE as-is)")
|
|
805
884
|
|
|
806
|
-
# 2. .env.example — create once at the repo root (cwd), never overwrite.
|
|
807
|
-
env_example = Path(".env.example")
|
|
808
|
-
if env_example.exists():
|
|
809
|
-
print(f"kept: {env_example} (already present)")
|
|
810
|
-
else:
|
|
811
|
-
env_example.write_text(env_example_text(), encoding="utf-8")
|
|
812
|
-
print(f"created: {env_example}")
|
|
813
|
-
|
|
814
885
|
# 3. suite.json — seed the eval-task sidecar next to archforge.py (so --root
|
|
815
886
|
# relocations also move the seeded suite); never overwrite — the user may have
|
|
816
887
|
# tuned the tasks. Byte-identical to the CLI's one-task fallback fixture.
|
|
@@ -820,10 +891,118 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|
|
820
891
|
else:
|
|
821
892
|
suite_path.write_text(_DEFAULT_SUITE_JSON, encoding="utf-8")
|
|
822
893
|
print(f"created: {suite_path} (edit the tasks to change what you optimize against)")
|
|
823
|
-
|
|
894
|
+
|
|
895
|
+
spec_path = pkg_dir / "spec.json"
|
|
896
|
+
print(f"\nNext: put your provider API key in a root `.env` (gitignored), edit the "
|
|
897
|
+
f"# EDIT: markers in {pkg_dir / 'app.py'} (your MAS's node roster/edges/knobs), "
|
|
898
|
+
f"run `{PROG} make-spec` to build + lint `archforge_optimizer/spec.json` from "
|
|
899
|
+
f"your edited adapter, then `{PROG} evolve --adapter "
|
|
900
|
+
f"archforge_optimizer.host:AppAdapter --seed {spec_path}`.")
|
|
901
|
+
return 0
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def _cmd_make_spec(args: argparse.Namespace) -> int:
|
|
905
|
+
"""Build + lint + write the bootstrap Spec JSON from the EDITED adapter.
|
|
906
|
+
|
|
907
|
+
Unlike `init` (which scaffolds a placeholder), this reads the user's actual MAS
|
|
908
|
+
details: it imports ``--adapter``, lets its app build the bootstrap Spec, runs the
|
|
909
|
+
Spec Linter on the result, and — only if it passes — writes it to ``--out``
|
|
910
|
+
(default ``archforge_optimizer/spec.json``). So the spec.json that ``evolve --seed``
|
|
911
|
+
consumes is the user's real roster, not a template. A failing lint returns rc=1
|
|
912
|
+
WITHOUT writing (the user fixes the # EDIT: markers and re-runs). cwd is on
|
|
913
|
+
sys.path (``main()`` puts it there before dispatch), so
|
|
914
|
+
``--adapter archforge_optimizer.host:AppAdapter`` resolves.
|
|
915
|
+
|
|
916
|
+
The app's ``build_spec()`` itself asserts-not-lint (it surfaces a malformed roster
|
|
917
|
+
loudly); for ``make-spec`` we want a clean rc=1 + the lint faults, not a raw
|
|
918
|
+
traceback. So a construction-time ``AssertionError`` is caught and its embedded
|
|
919
|
+
fault list is re-printed as the lint output."""
|
|
920
|
+
try:
|
|
921
|
+
_flush_adapter_cache(args.adapter) # always read the current cwd's files
|
|
922
|
+
host = _import_adapter(args.adapter)
|
|
923
|
+
except AssertionError as exc:
|
|
924
|
+
print(f"! {args.adapter}: built Spec fails the linter — not writing {args.out}:",
|
|
925
|
+
file=sys.stderr)
|
|
926
|
+
for fault in _parse_build_spec_assert(str(exc)):
|
|
927
|
+
print(f" {fault}", file=sys.stderr)
|
|
928
|
+
print(" edit the # EDIT: markers in your adapter app.py and re-run `make-spec`.",
|
|
929
|
+
file=sys.stderr)
|
|
930
|
+
return 1
|
|
931
|
+
# `app_spec()` is the adapter's published bootstrap Spec (LangGraphHostAdapter
|
|
932
|
+
# builds + caches it in __init__). It is not on the bare HostMAS protocol, so we
|
|
933
|
+
# surface a clear error for an adapter that lacks it rather than AttributeError.
|
|
934
|
+
build_spec = getattr(host, "app_spec", None)
|
|
935
|
+
if build_spec is None:
|
|
936
|
+
print(f"! {args.adapter} ({type(host).__name__}) exposes no app_spec() — "
|
|
937
|
+
f"make-spec needs a LangGraph-style adapter that builds a bootstrap Spec. "
|
|
938
|
+
f"Pass the host class (e.g. archforge_optimizer.host:AppAdapter).",
|
|
939
|
+
file=sys.stderr)
|
|
940
|
+
return 1
|
|
941
|
+
spec = build_spec()
|
|
942
|
+
faults = lint(spec)
|
|
943
|
+
if faults:
|
|
944
|
+
print(f"! {args.adapter}: built Spec fails the linter — not writing {args.out}:",
|
|
945
|
+
file=sys.stderr)
|
|
946
|
+
for f in faults:
|
|
947
|
+
loc = f" [{f.location}]" if f.location else ""
|
|
948
|
+
print(f" {f.code}{loc}: {f.message}", file=sys.stderr)
|
|
949
|
+
print(" edit the # EDIT: markers in your adapter app.py and re-run `make-spec`.",
|
|
950
|
+
file=sys.stderr)
|
|
951
|
+
return 1
|
|
952
|
+
out_path = Path(args.out)
|
|
953
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
954
|
+
out_path.write_text(spec.model_dump_json(indent=2), encoding="utf-8")
|
|
955
|
+
print(f"created: {out_path} (lint OK; run `{PROG} evolve --adapter "
|
|
956
|
+
f"{args.adapter} --seed {out_path}`)")
|
|
824
957
|
return 0
|
|
825
958
|
|
|
826
959
|
|
|
960
|
+
def _parse_build_spec_assert(message: str) -> list[str]:
|
|
961
|
+
"""Extract the per-fault strings from a ``build_spec()`` AssertionError message
|
|
962
|
+
(``LangGraph app Spec failed lint: ["code@loc: msg", ...]``). Falls back to the raw
|
|
963
|
+
tail of the message if the embedded list repr can't be parsed — never raises, so
|
|
964
|
+
``make-spec``'s error path stays crash-free on a malformed assert."""
|
|
965
|
+
import ast
|
|
966
|
+
marker = "failed lint:"
|
|
967
|
+
idx = message.find(marker)
|
|
968
|
+
if idx < 0:
|
|
969
|
+
return [message.strip()]
|
|
970
|
+
tail = message[idx + len(marker):].strip()
|
|
971
|
+
try:
|
|
972
|
+
parsed = ast.literal_eval(tail)
|
|
973
|
+
if isinstance(parsed, (list, tuple)) and all(isinstance(x, str) for x in parsed):
|
|
974
|
+
return list(parsed)
|
|
975
|
+
except (ValueError, SyntaxError):
|
|
976
|
+
pass
|
|
977
|
+
return [tail]
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def _flush_adapter_cache(dotted: str) -> None:
|
|
981
|
+
"""Drop a ``module:Class`` adapter's module subtree from ``sys.modules`` so the NEXT
|
|
982
|
+
import reads the *current cwd's* files, not a module cached from an earlier cwd.
|
|
983
|
+
|
|
984
|
+
Single-process reuse (the real CLI runs one command per process, so this is a no-op
|
|
985
|
+
there) defeats ``make-spec``/``evolve`` when the user edits ``app.py`` and re-runs
|
|
986
|
+
in the SAME process (e.g. the test harness, or a long-lived embedding): the stale
|
|
987
|
+
``archforge_optimizer`` — bound to a previous tmp cwd's files — would shadow the
|
|
988
|
+
edit. Flushing + ``importlib.invalidate_caches()`` makes \"build from the edited
|
|
989
|
+
adapter\" honest. Scoped to the adapter's own top-level package so core imports are
|
|
990
|
+
untouched."""
|
|
991
|
+
if ":" in dotted:
|
|
992
|
+
modpath = dotted.split(":", 1)[0]
|
|
993
|
+
else:
|
|
994
|
+
modpath = dotted
|
|
995
|
+
top = modpath.split(".", 1)[0]
|
|
996
|
+
stale = [name for name in list(sys.modules) if name == top or name.startswith(top + ".")]
|
|
997
|
+
for name in stale:
|
|
998
|
+
sys.modules.pop(name, None)
|
|
999
|
+
importlib.invalidate_caches()
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
|
|
827
1006
|
# --------------------------------------------------------------------------- #
|
|
828
1007
|
# entry
|
|
829
1008
|
# --------------------------------------------------------------------------- #
|
|
@@ -869,6 +1048,8 @@ def main(argv: list[str] | None = None, *,
|
|
|
869
1048
|
return _cmd_reject(args)
|
|
870
1049
|
if args.command == "init":
|
|
871
1050
|
return _cmd_init(args)
|
|
1051
|
+
if args.command == "make-spec":
|
|
1052
|
+
return _cmd_make_spec(args)
|
|
872
1053
|
if args.command == "evolve":
|
|
873
1054
|
return _cmd_evolve(args, components=components, loop=False)
|
|
874
1055
|
if args.command == "evolve-loop":
|
|
@@ -29,7 +29,7 @@ from pathlib import Path
|
|
|
29
29
|
# =========================================================================== #
|
|
30
30
|
# The package version. Read by hatchling for the built distribution and
|
|
31
31
|
# re-exported as `archforge.__version__` (archforge/__init__.py). One place.
|
|
32
|
-
VERSION: str = "0.
|
|
32
|
+
VERSION: str = "0.2.0"
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
# =========================================================================== #
|