atomadic-forge 0.3.2__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 (131) hide show
  1. atomadic_forge/__init__.py +12 -0
  2. atomadic_forge/__main__.py +5 -0
  3. atomadic_forge/a0_qk_constants/__init__.py +1 -0
  4. atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
  5. atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
  6. atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
  7. atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
  8. atomadic_forge/a0_qk_constants/error_codes.py +296 -0
  9. atomadic_forge/a0_qk_constants/forge_types.py +89 -0
  10. atomadic_forge/a0_qk_constants/gen_language.py +116 -0
  11. atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
  12. atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
  13. atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
  14. atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
  15. atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
  16. atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
  17. atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
  18. atomadic_forge/a0_qk_constants/tier_names.py +47 -0
  19. atomadic_forge/a1_at_functions/__init__.py +1 -0
  20. atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
  21. atomadic_forge/a1_at_functions/agent_memory.py +139 -0
  22. atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
  23. atomadic_forge/a1_at_functions/agent_summary.py +277 -0
  24. atomadic_forge/a1_at_functions/body_extractor.py +306 -0
  25. atomadic_forge/a1_at_functions/card_renderer.py +210 -0
  26. atomadic_forge/a1_at_functions/certify_checks.py +445 -0
  27. atomadic_forge/a1_at_functions/chat_context.py +170 -0
  28. atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
  29. atomadic_forge/a1_at_functions/classify_tier.py +115 -0
  30. atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
  31. atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
  32. atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
  33. atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
  34. atomadic_forge/a1_at_functions/config_io.py +68 -0
  35. atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
  36. atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
  37. atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
  38. atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
  39. atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
  40. atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
  41. atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
  42. atomadic_forge/a1_at_functions/error_hints.py +105 -0
  43. atomadic_forge/a1_at_functions/evolution_log.py +94 -0
  44. atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
  45. atomadic_forge/a1_at_functions/generation_quality.py +322 -0
  46. atomadic_forge/a1_at_functions/import_repair.py +211 -0
  47. atomadic_forge/a1_at_functions/import_smoke.py +102 -0
  48. atomadic_forge/a1_at_functions/js_parser.py +539 -0
  49. atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
  50. atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
  51. atomadic_forge/a1_at_functions/llm_client.py +554 -0
  52. atomadic_forge/a1_at_functions/local_signer.py +134 -0
  53. atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
  54. atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
  55. atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
  56. atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
  57. atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
  58. atomadic_forge/a1_at_functions/policy_loader.py +107 -0
  59. atomadic_forge/a1_at_functions/preflight_change.py +227 -0
  60. atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
  61. atomadic_forge/a1_at_functions/provider_detect.py +157 -0
  62. atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
  63. atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
  64. atomadic_forge/a1_at_functions/recipes.py +186 -0
  65. atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
  66. atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
  67. atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
  68. atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
  69. atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
  70. atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
  71. atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
  72. atomadic_forge/a1_at_functions/scout_walk.py +309 -0
  73. atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
  74. atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
  75. atomadic_forge/a1_at_functions/stub_detector.py +158 -0
  76. atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
  77. atomadic_forge/a1_at_functions/synergy_render.py +252 -0
  78. atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
  79. atomadic_forge/a1_at_functions/test_runner.py +196 -0
  80. atomadic_forge/a1_at_functions/test_selector.py +122 -0
  81. atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
  82. atomadic_forge/a1_at_functions/tool_composer.py +130 -0
  83. atomadic_forge/a1_at_functions/transcript_log.py +70 -0
  84. atomadic_forge/a1_at_functions/wire_check.py +260 -0
  85. atomadic_forge/a2_mo_composites/__init__.py +1 -0
  86. atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
  87. atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
  88. atomadic_forge/a2_mo_composites/plan_store.py +164 -0
  89. atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
  90. atomadic_forge/a3_og_features/__init__.py +1 -0
  91. atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
  92. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
  93. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
  94. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
  95. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
  96. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
  97. atomadic_forge/a3_og_features/demo_runner.py +502 -0
  98. atomadic_forge/a3_og_features/emergent_feature.py +95 -0
  99. atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
  100. atomadic_forge/a3_og_features/forge_enforce.py +107 -0
  101. atomadic_forge/a3_og_features/forge_evolve.py +176 -0
  102. atomadic_forge/a3_og_features/forge_loop.py +528 -0
  103. atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
  104. atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
  105. atomadic_forge/a3_og_features/lsp_server.py +98 -0
  106. atomadic_forge/a3_og_features/mcp_server.py +160 -0
  107. atomadic_forge/a3_og_features/setup_wizard.py +337 -0
  108. atomadic_forge/a3_og_features/synergy_feature.py +65 -0
  109. atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
  110. atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
  111. atomadic_forge/commands/__init__.py +1 -0
  112. atomadic_forge/commands/_registry.py +36 -0
  113. atomadic_forge/commands/audit.py +142 -0
  114. atomadic_forge/commands/chat.py +133 -0
  115. atomadic_forge/commands/commandsmith.py +178 -0
  116. atomadic_forge/commands/config_cmd.py +145 -0
  117. atomadic_forge/commands/demo.py +142 -0
  118. atomadic_forge/commands/emergent.py +124 -0
  119. atomadic_forge/commands/emergent_then_synergy.py +70 -0
  120. atomadic_forge/commands/evolve.py +122 -0
  121. atomadic_forge/commands/evolve_then_iterate.py +70 -0
  122. atomadic_forge/commands/feature_then_emergent.py +111 -0
  123. atomadic_forge/commands/iterate.py +140 -0
  124. atomadic_forge/commands/synergy.py +96 -0
  125. atomadic_forge/commands/synergy_then_emergent.py +70 -0
  126. atomadic_forge-0.3.2.dist-info/METADATA +471 -0
  127. atomadic_forge-0.3.2.dist-info/RECORD +131 -0
  128. atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
  129. atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
  130. atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
  131. atomadic_forge-0.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,107 @@
1
+ """Tier a1 — pure readers for .atomadic-forge lineage.jsonl + manifests.
2
+
3
+ Audit pain point (Lane D1): every ``--apply`` writes to lineage.jsonl,
4
+ but Forge ships no verb to query that file. Auditors and developers
5
+ have to ``cat`` and ``jq`` it. This module gives the CLI a clean
6
+ interface to surface what's there.
7
+
8
+ Pure: read-only, returns structured dicts. Side-effecting verbs
9
+ (``trend``, ``replay``) require lineage-shape extension and live in
10
+ their own lane; this module supports the inspection surface today.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from pathlib import Path
16
+
17
+ _DEFAULT_DIRNAME = ".atomadic-forge"
18
+ _LINEAGE_FILE = "lineage.jsonl"
19
+
20
+
21
+ def lineage_path(project_root: Path, *, dirname: str = _DEFAULT_DIRNAME) -> Path:
22
+ """Return the absolute path of the lineage log under ``project_root``."""
23
+ return Path(project_root).resolve() / dirname / _LINEAGE_FILE
24
+
25
+
26
+ def read_lineage(
27
+ project_root: Path,
28
+ *,
29
+ dirname: str = _DEFAULT_DIRNAME,
30
+ last: int | None = None,
31
+ ) -> list[dict]:
32
+ """Return lineage entries newest-last.
33
+
34
+ ``last`` (optional): when set, return only the most recent N entries.
35
+ Malformed lines are skipped silently — the lineage log is append-only
36
+ and a corrupt line should not break inspection of the rest.
37
+ """
38
+ log = lineage_path(project_root, dirname=dirname)
39
+ if not log.exists():
40
+ return []
41
+ entries: list[dict] = []
42
+ for line in log.read_text(encoding="utf-8").splitlines():
43
+ line = line.strip()
44
+ if not line:
45
+ continue
46
+ try:
47
+ entry = json.loads(line)
48
+ except json.JSONDecodeError:
49
+ continue
50
+ if isinstance(entry, dict):
51
+ entries.append(entry)
52
+ if last is not None and last >= 0:
53
+ entries = entries[-last:]
54
+ return entries
55
+
56
+
57
+ def list_artifacts(
58
+ project_root: Path,
59
+ *,
60
+ dirname: str = _DEFAULT_DIRNAME,
61
+ ) -> list[dict]:
62
+ """Return one summary entry per distinct artifact name.
63
+
64
+ Each summary names the artifact, its run count, the latest write
65
+ timestamp, and the relative path. Sorted by latest write descending.
66
+ """
67
+ by_name: dict[str, dict] = {}
68
+ for entry in read_lineage(project_root, dirname=dirname):
69
+ name = entry.get("artifact", "")
70
+ if not name:
71
+ continue
72
+ existing = by_name.get(name)
73
+ if existing is None:
74
+ by_name[name] = {
75
+ "artifact": name,
76
+ "run_count": 1,
77
+ "latest_ts_utc": entry.get("ts_utc", ""),
78
+ "path": entry.get("path", ""),
79
+ }
80
+ else:
81
+ existing["run_count"] += 1
82
+ ts = entry.get("ts_utc", "")
83
+ if ts > existing["latest_ts_utc"]:
84
+ existing["latest_ts_utc"] = ts
85
+ existing["path"] = entry.get("path", "")
86
+ return sorted(by_name.values(),
87
+ key=lambda d: d["latest_ts_utc"], reverse=True)
88
+
89
+
90
+ def load_manifest(
91
+ project_root: Path,
92
+ artifact: str,
93
+ *,
94
+ dirname: str = _DEFAULT_DIRNAME,
95
+ ) -> dict | None:
96
+ """Return the saved JSON manifest for ``artifact`` (e.g. 'scout',
97
+ 'cherry', 'wire', 'certify'). Returns None when no such manifest
98
+ has been written.
99
+ """
100
+ candidate = Path(project_root).resolve() / dirname / f"{artifact}.json"
101
+ if not candidate.exists():
102
+ return None
103
+ try:
104
+ data = json.loads(candidate.read_text(encoding="utf-8"))
105
+ except (OSError, json.JSONDecodeError):
106
+ return None
107
+ return data if isinstance(data, dict) else None
@@ -0,0 +1,554 @@
1
+ """Tier a1 — pluggable LLM client interface.
2
+
3
+ Forge is the architecture substrate. An LLM is the generation engine.
4
+ This module defines the contract between the two: a single ``call(prompt,
5
+ system) -> str`` interface, plus several ready-to-use implementations:
6
+
7
+ * :class:`AnthropicClient` — Claude. Reads ``ANTHROPIC_API_KEY``.
8
+ * :class:`OpenAIClient` — GPT. Reads ``OPENAI_API_KEY``.
9
+ * :class:`GeminiClient` — Google AI Studio (free tier). Reads ``GEMINI_API_KEY``.
10
+ * :class:`AAAANexusClient` — AAAA-Nexus inference (Cloudflare-Workers-AI
11
+ upstream, BitNet/HELIX wrappers, built-in hallucination guard). Reads
12
+ ``AAAA_NEXUS_API_KEY``. Each call is billed at $0.015 by the upstream
13
+ Nexus account — usage drives revenue while passing through the
14
+ anti-hallucination trust gate on every response.
15
+ * :class:`OllamaClient` — local. Talks to ``http://localhost:11434``.
16
+ * :class:`StubLLMClient` — deterministic, used by tests and dry-runs.
17
+
18
+ The client is intentionally minimal — Forge feeds it structured feedback
19
+ from wire/certify/emergent and asks for code in return. The LLM's job is
20
+ generation; Forge's job is keeping that generation in line with the
21
+ 5-tier law.
22
+
23
+ Free-tier note: ``gemini-2.5-flash`` and ``gemini-2.0-flash`` are free at
24
+ Google AI Studio (15 RPM, ~1500 RPD). Get a key at
25
+ https://aistudio.google.com/apikey then ``export GEMINI_API_KEY=…``.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import urllib.error
33
+ import urllib.request
34
+ from collections.abc import Callable
35
+ from typing import Protocol
36
+
37
+
38
+ class LLMClient(Protocol):
39
+ """Minimal contract every Forge LLM backend implements."""
40
+
41
+ name: str
42
+
43
+ def call(self, prompt: str, *, system: str = "",
44
+ max_tokens: int = 4096, temperature: float = 0.2) -> str:
45
+ ...
46
+
47
+
48
+ def _env_float(name: str, default: float) -> float:
49
+ raw = os.environ.get(name)
50
+ if raw is None or raw == "":
51
+ return default
52
+ try:
53
+ return float(raw)
54
+ except ValueError:
55
+ return default
56
+
57
+
58
+ def _env_int(name: str, default: int | None) -> int | None:
59
+ raw = os.environ.get(name)
60
+ if raw is None or raw == "":
61
+ return default
62
+ try:
63
+ return int(raw)
64
+ except ValueError:
65
+ return default
66
+
67
+
68
+ class StubLLMClient:
69
+ """Deterministic stub for tests + offline runs.
70
+
71
+ Configure with a ``responder(prompt, system) -> str`` callable, or pass
72
+ a list of canned responses for sequential calls.
73
+ """
74
+
75
+ name = "stub"
76
+
77
+ def __init__(self, *, responder: Callable[[str, str], str] | None = None,
78
+ canned: list[str] | None = None):
79
+ self._responder = responder
80
+ self._canned = list(canned or [])
81
+ self._calls = 0
82
+
83
+ def call(self, prompt: str, *, system: str = "",
84
+ max_tokens: int = 4096, temperature: float = 0.2) -> str:
85
+ self._calls += 1
86
+ if self._responder is not None:
87
+ return self._responder(prompt, system)
88
+ if self._canned:
89
+ return self._canned.pop(0) if self._canned else ""
90
+ return f"# stub response (call #{self._calls})\n"
91
+
92
+ @property
93
+ def calls(self) -> int:
94
+ return self._calls
95
+
96
+
97
+ class AnthropicClient:
98
+ """Claude via the Messages API."""
99
+
100
+ name = "anthropic"
101
+
102
+ def __init__(self, *, model: str = "claude-3-5-sonnet-latest",
103
+ api_key_env: str = "ANTHROPIC_API_KEY"):
104
+ self.model = model
105
+ self._api_key_env = api_key_env
106
+
107
+ def call(self, prompt: str, *, system: str = "",
108
+ max_tokens: int = 4096, temperature: float = 0.2) -> str:
109
+ api_key = os.environ.get(self._api_key_env, "")
110
+ if not api_key:
111
+ raise RuntimeError(
112
+ f"{self._api_key_env} not set — cannot call Anthropic API"
113
+ )
114
+ body = json.dumps({
115
+ "model": self.model,
116
+ "max_tokens": max_tokens,
117
+ "temperature": temperature,
118
+ "system": system or "You are a coding assistant.",
119
+ "messages": [{"role": "user", "content": prompt}],
120
+ }).encode("utf-8")
121
+ req = urllib.request.Request(
122
+ "https://api.anthropic.com/v1/messages",
123
+ data=body,
124
+ headers={
125
+ "x-api-key": api_key,
126
+ "anthropic-version": "2023-06-01",
127
+ "content-type": "application/json",
128
+ },
129
+ method="POST",
130
+ )
131
+ with urllib.request.urlopen(req, timeout=120) as resp:
132
+ data = json.loads(resp.read().decode("utf-8"))
133
+ # Concatenate text blocks; ignore tool-use blocks for this minimal client.
134
+ return "".join(b.get("text", "") for b in data.get("content", [])
135
+ if b.get("type") == "text")
136
+
137
+
138
+ class OpenAIClient:
139
+ """GPT via the Chat Completions API."""
140
+
141
+ name = "openai"
142
+
143
+ def __init__(self, *, model: str = "gpt-4o-mini",
144
+ api_key_env: str = "OPENAI_API_KEY"):
145
+ self.model = model
146
+ self._api_key_env = api_key_env
147
+
148
+ def call(self, prompt: str, *, system: str = "",
149
+ max_tokens: int = 4096, temperature: float = 0.2) -> str:
150
+ api_key = os.environ.get(self._api_key_env, "")
151
+ if not api_key:
152
+ raise RuntimeError(
153
+ f"{self._api_key_env} not set — cannot call OpenAI API"
154
+ )
155
+ messages = []
156
+ if system:
157
+ messages.append({"role": "system", "content": system})
158
+ messages.append({"role": "user", "content": prompt})
159
+ body = json.dumps({
160
+ "model": self.model,
161
+ "max_tokens": max_tokens,
162
+ "temperature": temperature,
163
+ "messages": messages,
164
+ }).encode("utf-8")
165
+ req = urllib.request.Request(
166
+ "https://api.openai.com/v1/chat/completions",
167
+ data=body,
168
+ headers={
169
+ "Authorization": f"Bearer {api_key}",
170
+ "Content-Type": "application/json",
171
+ },
172
+ method="POST",
173
+ )
174
+ with urllib.request.urlopen(req, timeout=120) as resp:
175
+ data = json.loads(resp.read().decode("utf-8"))
176
+ choices = data.get("choices") or []
177
+ return choices[0]["message"]["content"] if choices else ""
178
+
179
+
180
+ class GeminiClient:
181
+ """Google Gemini via the Generative Language API.
182
+
183
+ Free-tier models confirmed working (verified live during dev):
184
+ * ``gemini-2.5-flash`` — high-quality default (free tier)
185
+ * ``gemini-2.0-flash`` — older, more stable on free tier
186
+ * ``gemini-2.5-pro`` — better reasoning; check tier in AI Studio
187
+ * ``gemini-3.1-pro-preview`` — newest; check tier in AI Studio
188
+
189
+ Override the default via ``FORGE_GEMINI_MODEL=<id>`` env var. Concrete
190
+ rate limits live on your AI Studio dashboard:
191
+ https://aistudio.google.com/rate-limit
192
+ """
193
+
194
+ name = "gemini"
195
+
196
+ def __init__(self, *, model: str = "gemini-2.5-flash",
197
+ api_key_env: str = "GEMINI_API_KEY"):
198
+ self.model = model
199
+ self._api_key_env = api_key_env
200
+
201
+ def call(self, prompt: str, *, system: str = "",
202
+ max_tokens: int = 4096, temperature: float = 0.2) -> str:
203
+ api_key = (os.environ.get(self._api_key_env)
204
+ or os.environ.get("GOOGLE_API_KEY")
205
+ or "")
206
+ if not api_key:
207
+ raise RuntimeError(
208
+ f"{self._api_key_env} (or GOOGLE_API_KEY) not set — "
209
+ "get a free key at https://aistudio.google.com/apikey"
210
+ )
211
+ body_dict: dict[str, object] = {
212
+ "contents": [{"role": "user", "parts": [{"text": prompt}]}],
213
+ "generationConfig": {
214
+ "maxOutputTokens": max_tokens,
215
+ "temperature": temperature,
216
+ },
217
+ }
218
+ if system:
219
+ body_dict["systemInstruction"] = {"parts": [{"text": system}]}
220
+ body = json.dumps(body_dict).encode("utf-8")
221
+ url = (f"https://generativelanguage.googleapis.com/v1beta/models/"
222
+ f"{self.model}:generateContent?key={api_key}")
223
+ req = urllib.request.Request(
224
+ url,
225
+ data=body,
226
+ headers={"content-type": "application/json"},
227
+ method="POST",
228
+ )
229
+ last_error: Exception | None = None
230
+ for attempt in range(3):
231
+ try:
232
+ with urllib.request.urlopen(req, timeout=120) as resp:
233
+ data = json.loads(resp.read().decode("utf-8"))
234
+ break
235
+ except urllib.error.HTTPError as exc:
236
+ detail = exc.read().decode("utf-8", errors="replace")[:400]
237
+ if exc.code == 429:
238
+ raise RuntimeError(
239
+ "Gemini quota exceeded. Either: (1) wait for daily "
240
+ "reset, (2) use a different free key, or (3) fall "
241
+ "back to local generation with `--provider ollama`. "
242
+ f"Detail: {detail[:200]}"
243
+ ) from exc
244
+ if exc.code in (500, 502, 503, 504):
245
+ # transient — retry with exponential backoff
246
+ last_error = RuntimeError(f"Gemini {exc.code}: {detail[:200]}")
247
+ import time
248
+ time.sleep(1.5 ** attempt)
249
+ continue
250
+ raise RuntimeError(f"Gemini API error {exc.code}: {detail}") from exc
251
+ else:
252
+ raise last_error or RuntimeError("Gemini API: 3 retries exhausted")
253
+ candidates = data.get("candidates") or []
254
+ if not candidates:
255
+ return ""
256
+ parts = candidates[0].get("content", {}).get("parts", [])
257
+ return "".join(p.get("text", "") for p in parts)
258
+
259
+
260
+ class AAAANexusClient:
261
+ """AAAA-Nexus inference — Cloudflare-Workers-AI upstream with HELIX guards.
262
+
263
+ Hits ``POST /v1/inference`` on the public Nexus Worker. Every call:
264
+
265
+ * Goes through the anti-hallucination trust gate (returns confidence,
266
+ flagged-bool, trust-floor metadata in ``helix.anti_hallucination``).
267
+ * Is billed at $0.015 against the Nexus account that owns the API
268
+ key, so Forge usage drives Nexus revenue automatically.
269
+ * Falls back internally on the upstream side: AAAA_LLM service
270
+ binding → Cloudflare Workers AI REST. No client-side fallback
271
+ logic needed.
272
+
273
+ Configuration:
274
+ * ``AAAA_NEXUS_API_KEY`` — required Bearer token.
275
+ * ``AAAA_NEXUS_URL`` — override base URL (default: the public
276
+ Atomadic-Tech worker).
277
+ * ``AAAA_NEXUS_WRAPPER`` — choose ``helix-standard`` (default),
278
+ ``bitnet-standard``, or any wrapper
279
+ exposed by ``GET /v1/agents/capabilities``.
280
+
281
+ The response shape is OpenAI-compatible — ``choices[0].message.content``
282
+ is the only field Forge reads. HELIX/BitNet metadata (compression,
283
+ confidence, trust floor) is preserved on the raw response but not
284
+ returned by ``call()``; callers wanting that signal can subclass.
285
+ """
286
+
287
+ name = "aaaa-nexus"
288
+
289
+ DEFAULT_BASE_URL = "https://aaaa-nexus.atomadictech.workers.dev"
290
+ WRAPPER_PATHS = {
291
+ "helix-standard": "/v1/inference",
292
+ "helix-stream": "/v1/inference/stream",
293
+ "bitnet-standard": "/v1/bitnet/inference",
294
+ "bitnet-stream": "/v1/bitnet/inference/stream",
295
+ }
296
+
297
+ def __init__(self, *, base_url: str | None = None,
298
+ api_key_env: str = "AAAA_NEXUS_API_KEY",
299
+ wrapper: str | None = None):
300
+ self.base_url = (base_url
301
+ or os.environ.get("AAAA_NEXUS_URL")
302
+ or self.DEFAULT_BASE_URL).rstrip("/")
303
+ self._api_key_env = api_key_env
304
+ self.wrapper = (wrapper
305
+ or os.environ.get("AAAA_NEXUS_WRAPPER")
306
+ or "helix-standard")
307
+ if self.wrapper not in self.WRAPPER_PATHS:
308
+ raise ValueError(
309
+ f"unknown AAAA-Nexus wrapper: {self.wrapper!r} — expected "
310
+ f"one of {list(self.WRAPPER_PATHS)}"
311
+ )
312
+
313
+ def call(self, prompt: str, *, system: str = "",
314
+ max_tokens: int = 4096, temperature: float = 0.2) -> str:
315
+ api_key = os.environ.get(self._api_key_env, "")
316
+ if not api_key:
317
+ raise RuntimeError(
318
+ f"{self._api_key_env} not set — cannot call AAAA-Nexus inference"
319
+ )
320
+ messages = []
321
+ if system:
322
+ messages.append({"role": "system", "content": system})
323
+ messages.append({"role": "user", "content": prompt})
324
+ # The Nexus inference endpoint validates the body strictly — only
325
+ # ``messages`` is on the public schema today, extra fields like
326
+ # ``max_tokens`` / ``temperature`` get a 403. Keep the payload
327
+ # minimal; upstream picks reasonable defaults for the wrapper.
328
+ body = json.dumps({"messages": messages}).encode("utf-8")
329
+ endpoint = self.base_url + self.WRAPPER_PATHS[self.wrapper]
330
+ req = urllib.request.Request(
331
+ endpoint,
332
+ data=body,
333
+ headers={
334
+ # AAAA-Nexus auth: the storefront worker reads ``X-API-Key``
335
+ # (NOT ``Authorization: Bearer``) and matches against keys
336
+ # registered in NONCE_CACHE KV. Sending only Bearer falls
337
+ # through to the 3-call/day/IP trial path with HTTP 402.
338
+ # We send both headers so the same client also works against
339
+ # any future Bearer-style endpoint without a code change.
340
+ "X-API-Key": api_key,
341
+ "Authorization": f"Bearer {api_key}",
342
+ "Content-Type": "application/json",
343
+ # Cloudflare Worker WAF rejects urllib's default UA
344
+ # ("Python-urllib/3.x") with a 403. Identify ourselves
345
+ # explicitly so the request looks like a real client.
346
+ "User-Agent": "atomadic-forge/0.2 (AAAANexusClient)",
347
+ "Accept": "application/json",
348
+ },
349
+ method="POST",
350
+ )
351
+ with urllib.request.urlopen(req, timeout=180) as resp:
352
+ data = json.loads(resp.read().decode("utf-8"))
353
+ # If the upstream's hallucination guard flagged the response with
354
+ # very low confidence, surface it as a runtime warning — the loop
355
+ # will likely produce a poor file emit and we want the signal in
356
+ # logs rather than silently passing through.
357
+ helix = data.get("helix") or {}
358
+ ah = helix.get("anti_hallucination") or {}
359
+ if ah.get("flagged") and ah.get("confidence", 1.0) < 0.3:
360
+ # Soft warning — Forge's certify gate will catch quality issues
361
+ # downstream; we just emit a stderr breadcrumb here.
362
+ import sys as _sys
363
+ print(
364
+ f"[aaaa-nexus] anti-hallucination flagged response "
365
+ f"(confidence={ah.get('confidence'):.3f}, "
366
+ f"trust_floor={ah.get('trust_floor'):.3f})",
367
+ file=_sys.stderr,
368
+ )
369
+ choices = data.get("choices") or []
370
+ if not choices:
371
+ return ""
372
+ return choices[0].get("message", {}).get("content", "") or ""
373
+
374
+
375
+ class OllamaClient:
376
+ """Local Ollama daemon."""
377
+
378
+ name = "ollama"
379
+
380
+ def __init__(self, *, model: str = "qwen2.5-coder:7b",
381
+ base_url: str = "http://localhost:11434",
382
+ timeout_s: float | None = None,
383
+ num_predict: int | None = None):
384
+ self.model = model
385
+ self.base_url = base_url.rstrip("/")
386
+ self.timeout_s = timeout_s if timeout_s is not None else _env_float(
387
+ "FORGE_OLLAMA_TIMEOUT", 300.0
388
+ )
389
+ self.num_predict = num_predict if num_predict is not None else _env_int(
390
+ "FORGE_OLLAMA_NUM_PREDICT", None
391
+ )
392
+
393
+ def call(self, prompt: str, *, system: str = "",
394
+ max_tokens: int = 4096, temperature: float = 0.2) -> str:
395
+ num_predict = self.num_predict if self.num_predict is not None else max_tokens
396
+ body = json.dumps({
397
+ "model": self.model,
398
+ "stream": False,
399
+ "options": {"temperature": temperature, "num_predict": max(1, num_predict)},
400
+ "system": system,
401
+ "prompt": prompt,
402
+ }).encode("utf-8")
403
+ req = urllib.request.Request(
404
+ f"{self.base_url}/api/generate",
405
+ data=body,
406
+ headers={"content-type": "application/json"},
407
+ method="POST",
408
+ )
409
+ last_exc: Exception | None = None
410
+ for attempt in range(3):
411
+ try:
412
+ with urllib.request.urlopen(req, timeout=self.timeout_s) as resp:
413
+ data = json.loads(resp.read().decode("utf-8"))
414
+ return data.get("response", "")
415
+ except urllib.error.HTTPError as exc:
416
+ if exc.code in (500, 502, 503, 504):
417
+ import time
418
+ last_exc = RuntimeError(f"Ollama {exc.code} at {self.base_url}")
419
+ time.sleep(2.0 * (attempt + 1))
420
+ continue
421
+ raise RuntimeError(f"Ollama HTTP {exc.code}: {exc}") from exc
422
+ except urllib.error.URLError as exc:
423
+ raise RuntimeError(
424
+ f"Ollama unreachable at {self.base_url} — "
425
+ f"is `ollama serve` running? Detail: {exc}"
426
+ ) from exc
427
+ except TimeoutError as exc:
428
+ raise RuntimeError(
429
+ f"Ollama timed out after {self.timeout_s:g}s while using "
430
+ f"model {self.model!r}. Try a smaller local model, set "
431
+ "`FORGE_OLLAMA_NUM_PREDICT` lower, or raise "
432
+ "`FORGE_OLLAMA_TIMEOUT`."
433
+ ) from exc
434
+ raise last_exc or RuntimeError("Ollama: 3 retries exhausted")
435
+
436
+
437
+ class OpenRouterClient:
438
+ """OpenRouter — routes to 200+ models via OpenAI-compatible API.
439
+
440
+ Reads ``OPENROUTER_API_KEY``. Default model: ``deepseek/deepseek-chat-v3-0324:free``
441
+ (free tier). Override via ``FORGE_OPENROUTER_MODEL`` env var.
442
+ """
443
+
444
+ name = "openrouter"
445
+
446
+ BASE_URL = "https://openrouter.ai/api/v1/chat/completions"
447
+
448
+ def __init__(self, *, model: str | None = None,
449
+ api_key_env: str = "OPENROUTER_API_KEY"):
450
+ self.model = (model
451
+ or os.environ.get("FORGE_OPENROUTER_MODEL")
452
+ or "google/gemma-3-27b-it:free")
453
+ self._api_key_env = api_key_env
454
+
455
+ def call(self, prompt: str, *, system: str = "",
456
+ max_tokens: int = 4096, temperature: float = 0.2) -> str:
457
+ api_key = os.environ.get(self._api_key_env, "")
458
+ if not api_key:
459
+ raise RuntimeError(
460
+ f"{self._api_key_env} not set — cannot call OpenRouter API"
461
+ )
462
+ messages = []
463
+ if system:
464
+ messages.append({"role": "system", "content": system})
465
+ messages.append({"role": "user", "content": prompt})
466
+ body_dict = {
467
+ "model": self.model,
468
+ "max_tokens": max_tokens,
469
+ "temperature": temperature,
470
+ "messages": messages,
471
+ }
472
+ last_error: Exception | None = None
473
+ for attempt in range(3):
474
+ body = json.dumps(body_dict).encode("utf-8")
475
+ req = urllib.request.Request(
476
+ self.BASE_URL,
477
+ data=body,
478
+ headers={
479
+ "Authorization": f"Bearer {api_key}",
480
+ "Content-Type": "application/json",
481
+ "HTTP-Referer": "https://atomadic.tech",
482
+ "X-Title": "Atomadic Forge",
483
+ },
484
+ method="POST",
485
+ )
486
+ try:
487
+ with urllib.request.urlopen(req, timeout=120) as resp:
488
+ data = json.loads(resp.read().decode("utf-8"))
489
+ break
490
+ except urllib.error.HTTPError as exc:
491
+ detail = exc.read().decode("utf-8", errors="replace")[:600]
492
+ if exc.code == 400 and "instruction" in detail.lower() and attempt == 0:
493
+ # Model doesn't support system role — fold it into user turn.
494
+ merged = f"[System instructions]\n{system}\n\n[User]\n{prompt}" if system else prompt
495
+ body_dict = {
496
+ "model": self.model,
497
+ "max_tokens": max_tokens,
498
+ "temperature": temperature,
499
+ "messages": [{"role": "user", "content": merged}],
500
+ }
501
+ continue
502
+ if exc.code == 429:
503
+ import time
504
+ last_error = RuntimeError(f"OpenRouter 429: {detail[:200]}")
505
+ time.sleep(2.0 * (attempt + 1))
506
+ continue
507
+ if exc.code in (500, 502, 503, 504):
508
+ import time
509
+ last_error = RuntimeError(f"OpenRouter {exc.code}: {detail[:200]}")
510
+ time.sleep(1.5 ** attempt)
511
+ continue
512
+ raise RuntimeError(f"OpenRouter API error {exc.code}: {detail}") from exc
513
+ else:
514
+ raise last_error or RuntimeError("OpenRouter API: 3 retries exhausted")
515
+ choices = data.get("choices") or []
516
+ return choices[0]["message"]["content"] if choices else ""
517
+
518
+
519
+ def resolve_default_client() -> LLMClient:
520
+ """Pick a client based on which env var is set; fall back to stub.
521
+
522
+ Resolution order (sovereign-revenue-first → free-tier → paid → local):
523
+ 1. AAAA_NEXUS_API_KEY → AAAANexusClient (every call
524
+ bills upstream Nexus account
525
+ $0.015 with built-in
526
+ hallucination guard)
527
+ 2. ANTHROPIC_API_KEY → AnthropicClient
528
+ 3. GEMINI_API_KEY / GOOGLE_API_KEY → GeminiClient (free tier)
529
+ 4. OPENAI_API_KEY → OpenAIClient
530
+ 5. FORGE_OLLAMA=1 → OllamaClient (local, free)
531
+ 6. otherwise → StubLLMClient (offline)
532
+
533
+ AAAA-Nexus comes first because it's the only path that simultaneously
534
+ (a) generates revenue for the operator, (b) trust-gates every response
535
+ through the HELIX anti-hallucination layer, and (c) records the call
536
+ in the Nexus audit log automatically.
537
+ """
538
+ if os.environ.get("AAAA_NEXUS_API_KEY"):
539
+ return AAAANexusClient()
540
+ if os.environ.get("ANTHROPIC_API_KEY"):
541
+ return AnthropicClient()
542
+ if os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY"):
543
+ return GeminiClient(model=os.environ.get("FORGE_GEMINI_MODEL",
544
+ "gemini-2.5-flash"))
545
+ if os.environ.get("OPENAI_API_KEY"):
546
+ return OpenAIClient()
547
+ if os.environ.get("OPENROUTER_API_KEY"):
548
+ return OpenRouterClient()
549
+ if os.environ.get("FORGE_OLLAMA"):
550
+ return OllamaClient(model=os.environ.get("FORGE_OLLAMA_MODEL",
551
+ "qwen2.5-coder:7b"),
552
+ base_url=os.environ.get("OLLAMA_BASE_URL",
553
+ "http://localhost:11434"))
554
+ return StubLLMClient()