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.
Files changed (47) hide show
  1. archforge/__init__.py +76 -0
  2. archforge/__main__.py +10 -0
  3. archforge/architect.py +442 -0
  4. archforge/cli.py +881 -0
  5. archforge/config.py +140 -0
  6. archforge/config_init.py +150 -0
  7. archforge/diff.py +206 -0
  8. archforge/engine.py +444 -0
  9. archforge/gatekeeper.py +290 -0
  10. archforge/host/__init__.py +20 -0
  11. archforge/host/adapters/__init__.py +41 -0
  12. archforge/host/adapters/base.py +311 -0
  13. archforge/host/adapters/helpers.py +163 -0
  14. archforge/host/adapters/langgraph.py +726 -0
  15. archforge/host/base.py +105 -0
  16. archforge/host/fake.py +380 -0
  17. archforge/judge/__init__.py +20 -0
  18. archforge/judge/base.py +257 -0
  19. archforge/judge/scripted.py +145 -0
  20. archforge/lint.py +180 -0
  21. archforge/llm/__init__.py +65 -0
  22. archforge/llm/_common.py +94 -0
  23. archforge/llm/anthropic.py +90 -0
  24. archforge/llm/base.py +90 -0
  25. archforge/llm/gemini.py +112 -0
  26. archforge/llm/groq.py +63 -0
  27. archforge/llm/openai.py +63 -0
  28. archforge/llm/scripted.py +134 -0
  29. archforge/middleware.py +181 -0
  30. archforge/models.py +435 -0
  31. archforge/mutate.py +214 -0
  32. archforge/otel.py +613 -0
  33. archforge/runlog.py +103 -0
  34. archforge/runner.py +153 -0
  35. archforge/spec_builder.py +126 -0
  36. archforge/stores/__init__.py +22 -0
  37. archforge/stores/_jsonl.py +81 -0
  38. archforge/stores/attempt_store.py +161 -0
  39. archforge/stores/spec_store.py +188 -0
  40. archforge/stores/trace_store.py +42 -0
  41. archforge/suite.py +248 -0
  42. archforge/userconfig.py +144 -0
  43. archforge_optimizer-0.1.0.dist-info/METADATA +420 -0
  44. archforge_optimizer-0.1.0.dist-info/RECORD +47 -0
  45. archforge_optimizer-0.1.0.dist-info/WHEEL +4 -0
  46. archforge_optimizer-0.1.0.dist-info/entry_points.txt +2 -0
  47. 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"]