archforge-optimizer 0.1.0__py3-none-any.whl
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/__init__.py +76 -0
- archforge/__main__.py +10 -0
- archforge/architect.py +442 -0
- archforge/cli.py +881 -0
- archforge/config.py +140 -0
- archforge/config_init.py +150 -0
- archforge/diff.py +206 -0
- archforge/engine.py +444 -0
- archforge/gatekeeper.py +290 -0
- archforge/host/__init__.py +20 -0
- archforge/host/adapters/__init__.py +41 -0
- archforge/host/adapters/base.py +311 -0
- archforge/host/adapters/helpers.py +163 -0
- archforge/host/adapters/langgraph.py +726 -0
- archforge/host/base.py +105 -0
- archforge/host/fake.py +380 -0
- archforge/judge/__init__.py +20 -0
- archforge/judge/base.py +257 -0
- archforge/judge/scripted.py +145 -0
- archforge/lint.py +180 -0
- archforge/llm/__init__.py +65 -0
- archforge/llm/_common.py +94 -0
- archforge/llm/anthropic.py +90 -0
- archforge/llm/base.py +90 -0
- archforge/llm/gemini.py +112 -0
- archforge/llm/groq.py +63 -0
- archforge/llm/openai.py +63 -0
- archforge/llm/scripted.py +134 -0
- archforge/middleware.py +181 -0
- archforge/models.py +435 -0
- archforge/mutate.py +214 -0
- archforge/otel.py +613 -0
- archforge/runlog.py +103 -0
- archforge/runner.py +153 -0
- archforge/spec_builder.py +126 -0
- archforge/stores/__init__.py +22 -0
- archforge/stores/_jsonl.py +81 -0
- archforge/stores/attempt_store.py +161 -0
- archforge/stores/spec_store.py +188 -0
- archforge/stores/trace_store.py +42 -0
- archforge/suite.py +248 -0
- archforge/userconfig.py +144 -0
- archforge_optimizer-0.1.0.dist-info/METADATA +420 -0
- archforge_optimizer-0.1.0.dist-info/RECORD +47 -0
- archforge_optimizer-0.1.0.dist-info/WHEEL +4 -0
- archforge_optimizer-0.1.0.dist-info/entry_points.txt +2 -0
- archforge_optimizer-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Shared helpers for host adapters — the single source for the recurring
|
|
2
|
+
wiring every MAS adapter (and `FakeHostMAS`) needs.
|
|
3
|
+
|
|
4
|
+
These were lifted from two as-built implementations so the kit has ONE copy:
|
|
5
|
+
|
|
6
|
+
* `topo_order` / `run_id` — from `archforge.host.fake` (graph walk + run-id).
|
|
7
|
+
* `cfg_decay` — from the Lumina adapter's `_cfg` (the phase-2
|
|
8
|
+
"thread model/temp/max_tokens always, system_prompt only when mutated" rule).
|
|
9
|
+
|
|
10
|
+
This module is a pure leaf: it imports only `archforge.models` /
|
|
11
|
+
`archforge.config`, so importing it from `host/fake.py` (or any adapter)
|
|
12
|
+
cannot form a cycle.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
from collections import defaultdict
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import archforge.models as m
|
|
22
|
+
from archforge.config import SHORT_HASH_LEN
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# --------------------------------------------------------------------------- #
|
|
26
|
+
# Run identity
|
|
27
|
+
# --------------------------------------------------------------------------- #
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_id(spec_id: str, task_id: str, counter: int) -> str:
|
|
31
|
+
"""A unique, human-readable run_id: ``<spec hash>-<task>-<seq>``.
|
|
32
|
+
|
|
33
|
+
Uniqueness comes from the per-pipeline ``counter`` (one increment per
|
|
34
|
+
``run()`` call), so two repeats of the same (spec, task) — and two candidates
|
|
35
|
+
that share a spec_id — never collide. Determinism within a run comes from the
|
|
36
|
+
spec/task hash. (Semantics preserved from ``host/fake._run_id``.)
|
|
37
|
+
"""
|
|
38
|
+
h = hashlib.sha256(f"{spec_id}|{task_id}".encode()).hexdigest()[:SHORT_HASH_LEN]
|
|
39
|
+
return f"{h}-{task_id}-{counter:04d}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# --------------------------------------------------------------------------- #
|
|
43
|
+
# Execution order
|
|
44
|
+
# --------------------------------------------------------------------------- #
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def topo_order(spec: m.Spec) -> list[str]:
|
|
48
|
+
"""Kahn's algorithm over the (lint-clean, acyclic) Spec graph.
|
|
49
|
+
|
|
50
|
+
Roots (no in-edges) come first; frontier ties are broken by node insertion
|
|
51
|
+
order so the result is stable across runs — important for deterministic
|
|
52
|
+
traces. (Semantics preserved from ``host/fake._topo_order``.)
|
|
53
|
+
"""
|
|
54
|
+
ids = [n.node_id for n in spec.nodes]
|
|
55
|
+
adjacency: dict[str, list[str]] = defaultdict(list)
|
|
56
|
+
indegree: dict[str, int] = {nid: 0 for nid in ids}
|
|
57
|
+
for e in spec.edges:
|
|
58
|
+
if e.from_ == e.to:
|
|
59
|
+
continue
|
|
60
|
+
adjacency[e.from_].append(e.to)
|
|
61
|
+
indegree[e.to] += 1
|
|
62
|
+
order_index = {nid: i for i, nid in enumerate(ids)}
|
|
63
|
+
ready = sorted((nid for nid in ids if indegree[nid] == 0), key=lambda n: order_index[n])
|
|
64
|
+
out: list[str] = []
|
|
65
|
+
while ready:
|
|
66
|
+
n = ready.pop(0)
|
|
67
|
+
out.append(n)
|
|
68
|
+
for nxt in adjacency.get(n, []):
|
|
69
|
+
indegree[nxt] -= 1
|
|
70
|
+
if indegree[nxt] == 0:
|
|
71
|
+
ready.append(nxt)
|
|
72
|
+
ready.sort(key=lambda n: order_index[n])
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------------------------------- #
|
|
77
|
+
# Live-config decay — the phase-2 rule, centralized
|
|
78
|
+
# --------------------------------------------------------------------------- #
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class KnobVote:
|
|
83
|
+
"""What each live-knob becomes when the adapter hands it to its agent call.
|
|
84
|
+
|
|
85
|
+
``None`` on a field means "let the agent use its own hardcoded default" —
|
|
86
|
+
the agents' ``or``/``if X is None`` fallbacks honor that. So a base run
|
|
87
|
+
(the seeded Spec's values match the agents' consts) is behavior-identical to
|
|
88
|
+
the MAS running untouched, and a mutation (model_swap / knob / prompt_edit)
|
|
89
|
+
takes effect because the seeded value no longer equals the const.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
model: str | None
|
|
93
|
+
temperature: float | None
|
|
94
|
+
max_tokens: int | None
|
|
95
|
+
retries: int | None
|
|
96
|
+
system_prompt: str | None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cfg_decay(
|
|
100
|
+
node_id: str,
|
|
101
|
+
system_prompt: str | None,
|
|
102
|
+
model: str | None,
|
|
103
|
+
knobs: m.Knobs | None,
|
|
104
|
+
tools: list[str] | None, # noqa: ARG001 (kept on the seam for parity; unused here)
|
|
105
|
+
*,
|
|
106
|
+
base_prompts: dict[str, str] | None = None,
|
|
107
|
+
) -> KnobVote:
|
|
108
|
+
"""Live-Node-config → the per-call override an agent accepts.
|
|
109
|
+
|
|
110
|
+
Returns ``model`` / ``temperature`` / ``max_tokens`` / ``retries`` ALWAYS
|
|
111
|
+
(they equal the agents' hardcoded consts on a base run → identical
|
|
112
|
+
behaviour; they take effect when mutated), and ``system_prompt`` ONLY when
|
|
113
|
+
it differs from ``base_prompts[node_id]`` (an Architect ``prompt_edit``).
|
|
114
|
+
|
|
115
|
+
Passing a default ``system_prompt`` verbatim would corrupt MASes whose
|
|
116
|
+
prompts are runtime f-strings (Lumina's ``task``/``retrieval`` hold literal
|
|
117
|
+
``{placeholders}`` the static Spec can't resolve); a *mutated* prompt is a
|
|
118
|
+
complete replacement, so it rides through untouched. Base run unchanged;
|
|
119
|
+
``prompt_edit`` mutations honored.
|
|
120
|
+
"""
|
|
121
|
+
base = base_prompts or {}
|
|
122
|
+
sp_out = system_prompt if system_prompt != base.get(node_id) else None
|
|
123
|
+
return KnobVote(
|
|
124
|
+
model=model,
|
|
125
|
+
temperature=knobs.temperature if knobs is not None else None,
|
|
126
|
+
max_tokens=knobs.max_tokens if knobs is not None else None,
|
|
127
|
+
retries=knobs.retries if knobs is not None else None,
|
|
128
|
+
system_prompt=sp_out,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def cfg_as_kwargs(
|
|
133
|
+
vote: KnobVote,
|
|
134
|
+
*,
|
|
135
|
+
keys: tuple[str, ...] = ("model", "temperature", "max_tokens", "retries", "system_prompt"),
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
"""Flatten a ``KnobVote`` into the ``**kwargs`` shape an agent accepts.
|
|
138
|
+
|
|
139
|
+
The adapter declares ``keys`` so it binds EXACTLY the overrides its agent's
|
|
140
|
+
signature carries (the Lumina agents take model/temperature/max_tokens/
|
|
141
|
+
system_prompt but NOT retries); the kit owns the decay logic, not the
|
|
142
|
+
agent's signature. Default = the full set.
|
|
143
|
+
"""
|
|
144
|
+
out: dict[str, Any] = {}
|
|
145
|
+
if "model" in keys: out["model"] = vote.model
|
|
146
|
+
if "temperature" in keys: out["temperature"] = vote.temperature
|
|
147
|
+
if "max_tokens" in keys: out["max_tokens"] = vote.max_tokens
|
|
148
|
+
if "retries" in keys: out["retries"] = vote.retries
|
|
149
|
+
if "system_prompt" in keys: out["system_prompt"] = vote.system_prompt
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def estimate_tokens(text: str | None) -> int:
|
|
154
|
+
"""A rough, dependency-free token estimate for cost tracking.
|
|
155
|
+
|
|
156
|
+
The same ``len//4`` heuristic ``host/fake.FakeAgent`` uses inline; good enough
|
|
157
|
+
for budget caps and dedup. Real provider usage accounting is a later
|
|
158
|
+
fidelity item (out of scope for the kit), not a blocker here.
|
|
159
|
+
"""
|
|
160
|
+
return max(1, len(text or "") // 4)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
__all__ = ["run_id", "topo_order", "cfg_decay", "cfg_as_kwargs", "KnobVote", "estimate_tokens"]
|