@gaia-minds/assistant-cli 0.1.0 → 0.2.0
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.
- package/CONSTITUTION.md +208 -0
- package/README.md +85 -158
- package/assistant/README.md +126 -12
- package/package.json +6 -1
- package/tools/agent-actions.py +1213 -0
- package/tools/agent-alignment.py +888 -0
- package/tools/agent-config.yml +20 -5
- package/tools/agent-loop.py +502 -62
- package/tools/agent_actions.py +42 -0
- package/tools/agent_alignment.py +41 -0
- package/tools/gaia-assistant.py +2375 -34
package/tools/agent-loop.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
This is the reasoning core of an autonomous agent that:
|
|
5
5
|
1. Gathers state from the repository and GitHub
|
|
6
|
-
2. Asks
|
|
6
|
+
2. Asks the configured reasoning provider what to do
|
|
7
7
|
3. Checks alignment of proposed actions
|
|
8
8
|
4. Executes approved actions
|
|
9
9
|
5. Writes memory (decisions, lessons, state)
|
|
@@ -25,6 +25,8 @@ import logging
|
|
|
25
25
|
import os
|
|
26
26
|
import sys
|
|
27
27
|
import time
|
|
28
|
+
import urllib.error
|
|
29
|
+
import urllib.request
|
|
28
30
|
from datetime import datetime, timezone
|
|
29
31
|
from pathlib import Path
|
|
30
32
|
from typing import Any, Dict, List, Optional
|
|
@@ -41,7 +43,6 @@ sys.path.insert(0, str(SCRIPT_DIR))
|
|
|
41
43
|
from agent_alignment import ( # noqa: E402
|
|
42
44
|
AlignmentResult,
|
|
43
45
|
check_alignment,
|
|
44
|
-
classify_risk,
|
|
45
46
|
load_constitution,
|
|
46
47
|
)
|
|
47
48
|
from agent_actions import ( # noqa: E402
|
|
@@ -118,13 +119,20 @@ DEFAULT_BUDGET_POLICY: Dict[str, Any] = {
|
|
|
118
119
|
"hard_cycle_token_cap": 12000,
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5-20250929"
|
|
123
|
+
DEFAULT_OPENAI_MODEL = "gpt-4.1-mini"
|
|
124
|
+
DEFAULT_OPENROUTER_MODEL = "openrouter/auto"
|
|
125
|
+
SUPPORTED_REASONING_PROVIDERS = {"anthropic", "openai", "openrouter"}
|
|
126
|
+
|
|
121
127
|
|
|
122
128
|
# ---------------------------------------------------------------------------
|
|
123
129
|
# Config
|
|
124
130
|
# ---------------------------------------------------------------------------
|
|
125
131
|
|
|
126
132
|
CONFIG_PATH = SCRIPT_DIR / "agent-config.yml"
|
|
127
|
-
MEMORY_DIR =
|
|
133
|
+
MEMORY_DIR = Path(
|
|
134
|
+
os.environ.get("GAIA_AGENT_MEMORY_DIR", str(SCRIPT_DIR / "agent-memory"))
|
|
135
|
+
).expanduser()
|
|
128
136
|
DECISIONS_PATH = MEMORY_DIR / "decisions.jsonl"
|
|
129
137
|
LESSONS_PATH = MEMORY_DIR / "lessons.jsonl"
|
|
130
138
|
STATE_PATH = MEMORY_DIR / "state.json"
|
|
@@ -146,6 +154,181 @@ def load_config(path: Path = CONFIG_PATH) -> Dict[str, Any]:
|
|
|
146
154
|
return config
|
|
147
155
|
|
|
148
156
|
|
|
157
|
+
def _resolve_reasoning_provider(config: Dict[str, Any]) -> str:
|
|
158
|
+
"""Resolve reasoning provider from env override or config."""
|
|
159
|
+
override = os.environ.get("GAIA_REASONING_PROVIDER", "").strip().lower()
|
|
160
|
+
if override:
|
|
161
|
+
if override in SUPPORTED_REASONING_PROVIDERS:
|
|
162
|
+
return override
|
|
163
|
+
log.warning(
|
|
164
|
+
"Unsupported GAIA_REASONING_PROVIDER=%s; falling back to config/default",
|
|
165
|
+
override,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
reasoning_cfg = config.get("reasoning", {})
|
|
169
|
+
if not isinstance(reasoning_cfg, dict):
|
|
170
|
+
reasoning_cfg = {}
|
|
171
|
+
configured = str(reasoning_cfg.get("provider", "anthropic")).strip().lower()
|
|
172
|
+
if configured in SUPPORTED_REASONING_PROVIDERS:
|
|
173
|
+
return configured
|
|
174
|
+
return "anthropic"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _resolve_reasoning_model(config: Dict[str, Any], provider: str) -> str:
|
|
178
|
+
"""Resolve reasoning model from env overrides and config."""
|
|
179
|
+
direct_override = os.environ.get("GAIA_REASONING_MODEL", "").strip()
|
|
180
|
+
if direct_override:
|
|
181
|
+
return direct_override
|
|
182
|
+
|
|
183
|
+
reasoning = config.get("reasoning", {})
|
|
184
|
+
reasoning = reasoning if isinstance(reasoning, dict) else {}
|
|
185
|
+
models_cfg = reasoning.get("models", {})
|
|
186
|
+
models_cfg = models_cfg if isinstance(models_cfg, dict) else {}
|
|
187
|
+
|
|
188
|
+
if provider == "openai":
|
|
189
|
+
env_model = os.environ.get("OPENAI_MODEL", "").strip()
|
|
190
|
+
if env_model:
|
|
191
|
+
return env_model
|
|
192
|
+
|
|
193
|
+
configured_openai_model = str(models_cfg.get("openai", "")).strip()
|
|
194
|
+
if configured_openai_model:
|
|
195
|
+
return configured_openai_model
|
|
196
|
+
|
|
197
|
+
openai_cfg = reasoning.get("openai", {})
|
|
198
|
+
if isinstance(openai_cfg, dict):
|
|
199
|
+
openai_model = str(openai_cfg.get("model", "")).strip()
|
|
200
|
+
if openai_model:
|
|
201
|
+
return openai_model
|
|
202
|
+
|
|
203
|
+
legacy_model = str(reasoning.get("model", "")).strip()
|
|
204
|
+
if legacy_model and legacy_model != DEFAULT_ANTHROPIC_MODEL:
|
|
205
|
+
return legacy_model
|
|
206
|
+
|
|
207
|
+
return DEFAULT_OPENAI_MODEL
|
|
208
|
+
|
|
209
|
+
if provider == "openrouter":
|
|
210
|
+
env_model = os.environ.get("OPENROUTER_MODEL", "").strip()
|
|
211
|
+
if env_model:
|
|
212
|
+
return env_model
|
|
213
|
+
|
|
214
|
+
configured_openrouter_model = str(models_cfg.get("openrouter", "")).strip()
|
|
215
|
+
if configured_openrouter_model:
|
|
216
|
+
return configured_openrouter_model
|
|
217
|
+
|
|
218
|
+
openrouter_cfg = reasoning.get("openrouter", {})
|
|
219
|
+
if isinstance(openrouter_cfg, dict):
|
|
220
|
+
openrouter_model = str(openrouter_cfg.get("model", "")).strip()
|
|
221
|
+
if openrouter_model:
|
|
222
|
+
return openrouter_model
|
|
223
|
+
|
|
224
|
+
legacy_model = str(reasoning.get("model", "")).strip()
|
|
225
|
+
if legacy_model and legacy_model != DEFAULT_ANTHROPIC_MODEL:
|
|
226
|
+
return legacy_model
|
|
227
|
+
|
|
228
|
+
return DEFAULT_OPENROUTER_MODEL
|
|
229
|
+
|
|
230
|
+
env_model = os.environ.get("ANTHROPIC_MODEL", "").strip()
|
|
231
|
+
if env_model:
|
|
232
|
+
return env_model
|
|
233
|
+
|
|
234
|
+
configured_anthropic_model = str(models_cfg.get("anthropic", "")).strip()
|
|
235
|
+
if configured_anthropic_model:
|
|
236
|
+
return configured_anthropic_model
|
|
237
|
+
|
|
238
|
+
legacy_model = str(reasoning.get("model", "")).strip()
|
|
239
|
+
return legacy_model or DEFAULT_ANTHROPIC_MODEL
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _resolve_openai_runtime(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
243
|
+
"""Resolve OpenAI runtime settings from config and env."""
|
|
244
|
+
reasoning = config.get("reasoning", {})
|
|
245
|
+
reasoning = reasoning if isinstance(reasoning, dict) else {}
|
|
246
|
+
openai_cfg = reasoning.get("openai", {})
|
|
247
|
+
openai_cfg = openai_cfg if isinstance(openai_cfg, dict) else {}
|
|
248
|
+
|
|
249
|
+
base_url = str(
|
|
250
|
+
os.environ.get(
|
|
251
|
+
"OPENAI_BASE_URL",
|
|
252
|
+
openai_cfg.get("base_url", "https://api.openai.com/v1"),
|
|
253
|
+
)
|
|
254
|
+
).strip()
|
|
255
|
+
if not base_url:
|
|
256
|
+
base_url = "https://api.openai.com/v1"
|
|
257
|
+
base_url = base_url.rstrip("/")
|
|
258
|
+
|
|
259
|
+
timeout_raw = os.environ.get(
|
|
260
|
+
"OPENAI_TIMEOUT_SECONDS",
|
|
261
|
+
str(openai_cfg.get("timeout_seconds", 120)),
|
|
262
|
+
)
|
|
263
|
+
try:
|
|
264
|
+
timeout_seconds = int(str(timeout_raw).strip())
|
|
265
|
+
except ValueError:
|
|
266
|
+
timeout_seconds = 120
|
|
267
|
+
if timeout_seconds < 1:
|
|
268
|
+
timeout_seconds = 120
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"api_key": os.environ.get("OPENAI_API_KEY", "").strip(),
|
|
272
|
+
"base_url": base_url,
|
|
273
|
+
"timeout_seconds": timeout_seconds,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _resolve_openrouter_runtime(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
278
|
+
"""Resolve OpenRouter runtime settings from config and env."""
|
|
279
|
+
reasoning = config.get("reasoning", {})
|
|
280
|
+
reasoning = reasoning if isinstance(reasoning, dict) else {}
|
|
281
|
+
openrouter_cfg = reasoning.get("openrouter", {})
|
|
282
|
+
openrouter_cfg = openrouter_cfg if isinstance(openrouter_cfg, dict) else {}
|
|
283
|
+
|
|
284
|
+
base_url = str(
|
|
285
|
+
os.environ.get(
|
|
286
|
+
"OPENROUTER_BASE_URL",
|
|
287
|
+
openrouter_cfg.get("base_url", "https://openrouter.ai/api/v1"),
|
|
288
|
+
)
|
|
289
|
+
).strip()
|
|
290
|
+
if not base_url:
|
|
291
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
292
|
+
base_url = base_url.rstrip("/")
|
|
293
|
+
|
|
294
|
+
app_name = str(
|
|
295
|
+
os.environ.get(
|
|
296
|
+
"OPENROUTER_APP_NAME",
|
|
297
|
+
openrouter_cfg.get("app_name", "gaia-minds-agent"),
|
|
298
|
+
)
|
|
299
|
+
).strip()
|
|
300
|
+
if not app_name:
|
|
301
|
+
app_name = "gaia-minds-agent"
|
|
302
|
+
|
|
303
|
+
app_url = str(
|
|
304
|
+
os.environ.get(
|
|
305
|
+
"OPENROUTER_APP_URL",
|
|
306
|
+
openrouter_cfg.get("app_url", "https://github.com/Gaia-minds/gaia-minds"),
|
|
307
|
+
)
|
|
308
|
+
).strip()
|
|
309
|
+
if not app_url:
|
|
310
|
+
app_url = "https://github.com/Gaia-minds/gaia-minds"
|
|
311
|
+
|
|
312
|
+
timeout_raw = os.environ.get(
|
|
313
|
+
"OPENROUTER_TIMEOUT_SECONDS",
|
|
314
|
+
str(openrouter_cfg.get("timeout_seconds", 120)),
|
|
315
|
+
)
|
|
316
|
+
try:
|
|
317
|
+
timeout_seconds = int(str(timeout_raw).strip())
|
|
318
|
+
except ValueError:
|
|
319
|
+
timeout_seconds = 120
|
|
320
|
+
if timeout_seconds < 1:
|
|
321
|
+
timeout_seconds = 120
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
"api_key": os.environ.get("OPENROUTER_API_KEY", "").strip(),
|
|
325
|
+
"base_url": base_url,
|
|
326
|
+
"app_name": app_name,
|
|
327
|
+
"app_url": app_url,
|
|
328
|
+
"timeout_seconds": timeout_seconds,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
149
332
|
# ---------------------------------------------------------------------------
|
|
150
333
|
# Memory
|
|
151
334
|
# ---------------------------------------------------------------------------
|
|
@@ -388,7 +571,7 @@ def action_allowed_in_track(action_type: str, active_track: str, config: Dict[st
|
|
|
388
571
|
|
|
389
572
|
|
|
390
573
|
# ---------------------------------------------------------------------------
|
|
391
|
-
#
|
|
574
|
+
# Reasoning
|
|
392
575
|
# ---------------------------------------------------------------------------
|
|
393
576
|
|
|
394
577
|
SYSTEM_PROMPT_TEMPLATE = """\
|
|
@@ -480,19 +663,168 @@ def state_to_summary(state: RepoState) -> Dict[str, Any]:
|
|
|
480
663
|
}
|
|
481
664
|
|
|
482
665
|
|
|
483
|
-
def
|
|
666
|
+
def _strip_markdown_fences(text: str) -> str:
|
|
667
|
+
"""Remove markdown code fences from LLM text responses."""
|
|
668
|
+
out = text.strip()
|
|
669
|
+
if out.startswith("```"):
|
|
670
|
+
lines = out.splitlines()
|
|
671
|
+
lines = [line for line in lines if not line.strip().startswith("```")]
|
|
672
|
+
out = "\n".join(lines)
|
|
673
|
+
return out.strip()
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _extract_anthropic_text(response: Any) -> str:
|
|
677
|
+
"""Extract plain text from an Anthropic response object."""
|
|
678
|
+
text = ""
|
|
679
|
+
for block in response.content:
|
|
680
|
+
if hasattr(block, "text"):
|
|
681
|
+
text += block.text
|
|
682
|
+
return text
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _extract_chat_completion_text(payload: Dict[str, Any], provider_name: str) -> str:
|
|
686
|
+
"""Extract assistant message text from chat completion payload."""
|
|
687
|
+
choices = payload.get("choices", [])
|
|
688
|
+
if not isinstance(choices, list) or not choices:
|
|
689
|
+
raise ValueError(f"{provider_name} response missing choices")
|
|
690
|
+
message = choices[0].get("message", {}) if isinstance(choices[0], dict) else {}
|
|
691
|
+
if not isinstance(message, dict):
|
|
692
|
+
raise ValueError(f"{provider_name} response has invalid message payload")
|
|
693
|
+
content = message.get("content", "")
|
|
694
|
+
if isinstance(content, str):
|
|
695
|
+
return content
|
|
696
|
+
if isinstance(content, list):
|
|
697
|
+
parts: List[str] = []
|
|
698
|
+
for item in content:
|
|
699
|
+
if isinstance(item, dict):
|
|
700
|
+
text_value = item.get("text")
|
|
701
|
+
if isinstance(text_value, str):
|
|
702
|
+
parts.append(text_value)
|
|
703
|
+
return "".join(parts)
|
|
704
|
+
raise ValueError(f"{provider_name} response content is not text")
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _openai_chat_completion(
|
|
708
|
+
client: Dict[str, Any],
|
|
709
|
+
model: str,
|
|
710
|
+
system_prompt: str,
|
|
711
|
+
user_prompt: str,
|
|
712
|
+
max_tokens: int,
|
|
713
|
+
temperature: float,
|
|
714
|
+
) -> str:
|
|
715
|
+
"""Call OpenAI chat completions API and return text response."""
|
|
716
|
+
endpoint = f"{client['base_url']}/chat/completions"
|
|
717
|
+
payload = {
|
|
718
|
+
"model": model,
|
|
719
|
+
"messages": [
|
|
720
|
+
{"role": "system", "content": system_prompt},
|
|
721
|
+
{"role": "user", "content": user_prompt},
|
|
722
|
+
],
|
|
723
|
+
"max_tokens": max_tokens,
|
|
724
|
+
"temperature": temperature,
|
|
725
|
+
}
|
|
726
|
+
headers = {
|
|
727
|
+
"Authorization": f"Bearer {client['api_key']}",
|
|
728
|
+
"Content-Type": "application/json",
|
|
729
|
+
}
|
|
730
|
+
request = urllib.request.Request(
|
|
731
|
+
endpoint,
|
|
732
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
733
|
+
headers=headers,
|
|
734
|
+
method="POST",
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
try:
|
|
738
|
+
with urllib.request.urlopen(request, timeout=int(client["timeout_seconds"])) as response:
|
|
739
|
+
raw = response.read().decode("utf-8")
|
|
740
|
+
except urllib.error.HTTPError as exc:
|
|
741
|
+
details = exc.read().decode("utf-8", errors="replace")
|
|
742
|
+
raise RuntimeError(f"OpenAI API returned HTTP {exc.code}: {details[:400]}") from exc
|
|
743
|
+
except urllib.error.URLError as exc:
|
|
744
|
+
raise RuntimeError(f"OpenAI request failed: {exc}") from exc
|
|
745
|
+
|
|
746
|
+
try:
|
|
747
|
+
parsed = json.loads(raw)
|
|
748
|
+
except json.JSONDecodeError as exc:
|
|
749
|
+
raise RuntimeError("OpenAI API returned non-JSON response") from exc
|
|
750
|
+
|
|
751
|
+
return _extract_chat_completion_text(parsed, "OpenAI")
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _openrouter_chat_completion(
|
|
755
|
+
client: Dict[str, Any],
|
|
756
|
+
model: str,
|
|
757
|
+
system_prompt: str,
|
|
758
|
+
user_prompt: str,
|
|
759
|
+
max_tokens: int,
|
|
760
|
+
temperature: float,
|
|
761
|
+
) -> str:
|
|
762
|
+
"""Call OpenRouter chat completions API and return text response."""
|
|
763
|
+
endpoint = f"{client['base_url']}/chat/completions"
|
|
764
|
+
payload = {
|
|
765
|
+
"model": model,
|
|
766
|
+
"messages": [
|
|
767
|
+
{"role": "system", "content": system_prompt},
|
|
768
|
+
{"role": "user", "content": user_prompt},
|
|
769
|
+
],
|
|
770
|
+
"max_tokens": max_tokens,
|
|
771
|
+
"temperature": temperature,
|
|
772
|
+
}
|
|
773
|
+
headers = {
|
|
774
|
+
"Authorization": f"Bearer {client['api_key']}",
|
|
775
|
+
"Content-Type": "application/json",
|
|
776
|
+
"HTTP-Referer": client["app_url"],
|
|
777
|
+
"X-Title": client["app_name"],
|
|
778
|
+
}
|
|
779
|
+
request = urllib.request.Request(
|
|
780
|
+
endpoint,
|
|
781
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
782
|
+
headers=headers,
|
|
783
|
+
method="POST",
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
try:
|
|
787
|
+
with urllib.request.urlopen(request, timeout=int(client["timeout_seconds"])) as response:
|
|
788
|
+
raw = response.read().decode("utf-8")
|
|
789
|
+
except urllib.error.HTTPError as exc:
|
|
790
|
+
details = exc.read().decode("utf-8", errors="replace")
|
|
791
|
+
raise RuntimeError(f"OpenRouter API returned HTTP {exc.code}: {details[:400]}") from exc
|
|
792
|
+
except urllib.error.URLError as exc:
|
|
793
|
+
raise RuntimeError(f"OpenRouter request failed: {exc}") from exc
|
|
794
|
+
|
|
795
|
+
try:
|
|
796
|
+
parsed = json.loads(raw)
|
|
797
|
+
except json.JSONDecodeError as exc:
|
|
798
|
+
raise RuntimeError("OpenRouter API returned non-JSON response") from exc
|
|
799
|
+
|
|
800
|
+
return _extract_chat_completion_text(parsed, "OpenRouter")
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def ask_model_for_plan(
|
|
484
804
|
client: Any,
|
|
485
805
|
config: Dict[str, Any],
|
|
486
806
|
state: RepoState,
|
|
487
807
|
memory: Dict[str, Any],
|
|
488
808
|
constitution: str,
|
|
489
809
|
active_track: str,
|
|
810
|
+
provider: str,
|
|
811
|
+
model: str,
|
|
490
812
|
) -> Dict[str, Any]:
|
|
491
|
-
"""Ask
|
|
813
|
+
"""Ask the configured reasoning provider to analyze state and propose actions."""
|
|
492
814
|
reasoning_config = config.get("reasoning", {})
|
|
493
|
-
|
|
815
|
+
reasoning_config = reasoning_config if isinstance(reasoning_config, dict) else {}
|
|
494
816
|
max_tokens = reasoning_config.get("max_tokens", 4096)
|
|
495
817
|
temperature = reasoning_config.get("temperature", 0.3)
|
|
818
|
+
try:
|
|
819
|
+
max_tokens_int = int(max_tokens)
|
|
820
|
+
except (TypeError, ValueError):
|
|
821
|
+
max_tokens_int = 4096
|
|
822
|
+
if max_tokens_int < 1:
|
|
823
|
+
max_tokens_int = 4096
|
|
824
|
+
try:
|
|
825
|
+
temperature_float = float(temperature)
|
|
826
|
+
except (TypeError, ValueError):
|
|
827
|
+
temperature_float = 0.3
|
|
496
828
|
|
|
497
829
|
system_prompt = SYSTEM_PROMPT_TEMPLATE.format(constitution=constitution)
|
|
498
830
|
|
|
@@ -511,42 +843,62 @@ def ask_claude_for_plan(
|
|
|
511
843
|
budget_policy_json=json.dumps(budget_policy, indent=2),
|
|
512
844
|
)
|
|
513
845
|
|
|
514
|
-
log.info("Asking
|
|
846
|
+
log.info("Asking reasoning provider '%s' (%s) for a plan...", provider, model)
|
|
515
847
|
log.debug("System prompt: %d chars, User prompt: %d chars", len(system_prompt), len(user_prompt))
|
|
516
848
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
849
|
+
text: str
|
|
850
|
+
try:
|
|
851
|
+
if provider == "anthropic":
|
|
852
|
+
response = client.messages.create(
|
|
853
|
+
model=model,
|
|
854
|
+
max_tokens=max_tokens_int,
|
|
855
|
+
temperature=temperature_float,
|
|
856
|
+
system=system_prompt,
|
|
857
|
+
messages=[{"role": "user", "content": user_prompt}],
|
|
858
|
+
)
|
|
859
|
+
text = _extract_anthropic_text(response)
|
|
860
|
+
elif provider == "openai":
|
|
861
|
+
if not isinstance(client, dict):
|
|
862
|
+
raise RuntimeError("OpenAI client is misconfigured")
|
|
863
|
+
text = _openai_chat_completion(
|
|
864
|
+
client=client,
|
|
865
|
+
model=model,
|
|
866
|
+
system_prompt=system_prompt,
|
|
867
|
+
user_prompt=user_prompt,
|
|
868
|
+
max_tokens=max_tokens_int,
|
|
869
|
+
temperature=temperature_float,
|
|
870
|
+
)
|
|
871
|
+
elif provider == "openrouter":
|
|
872
|
+
if not isinstance(client, dict):
|
|
873
|
+
raise RuntimeError("OpenRouter client is misconfigured")
|
|
874
|
+
text = _openrouter_chat_completion(
|
|
875
|
+
client=client,
|
|
876
|
+
model=model,
|
|
877
|
+
system_prompt=system_prompt,
|
|
878
|
+
user_prompt=user_prompt,
|
|
879
|
+
max_tokens=max_tokens_int,
|
|
880
|
+
temperature=temperature_float,
|
|
881
|
+
)
|
|
882
|
+
else:
|
|
883
|
+
raise RuntimeError(f"Unsupported reasoning provider: {provider}")
|
|
884
|
+
except Exception as exc:
|
|
885
|
+
log.error("Reasoning request failed (%s/%s): %s", provider, model, exc)
|
|
886
|
+
return {"reasoning": f"Reasoning request failed: {exc}", "actions": []}
|
|
530
887
|
|
|
531
|
-
# Parse JSON from response
|
|
532
|
-
|
|
533
|
-
text = text.strip()
|
|
534
|
-
if text.startswith("```"):
|
|
535
|
-
# Remove code fences
|
|
536
|
-
lines = text.splitlines()
|
|
537
|
-
lines = [l for l in lines if not l.strip().startswith("```")]
|
|
538
|
-
text = "\n".join(lines)
|
|
888
|
+
# Parse JSON from response text.
|
|
889
|
+
text = _strip_markdown_fences(text)
|
|
539
890
|
|
|
540
891
|
try:
|
|
541
892
|
plan = json.loads(text)
|
|
542
893
|
except json.JSONDecodeError as exc:
|
|
543
|
-
log.error("Failed to parse
|
|
894
|
+
log.error("Failed to parse reasoning response as JSON: %s", exc)
|
|
544
895
|
log.error("Raw response: %s", text[:500])
|
|
545
896
|
plan = {"reasoning": f"Failed to parse response: {exc}", "actions": []}
|
|
546
897
|
|
|
547
898
|
actions = plan.get("actions", [])
|
|
548
899
|
log.info(
|
|
549
|
-
"
|
|
900
|
+
"Reasoning provider '%s' proposed %d action(s): %s",
|
|
901
|
+
provider,
|
|
550
902
|
len(actions),
|
|
551
903
|
", ".join(a.get("type", "?") for a in actions) or "(none)",
|
|
552
904
|
)
|
|
@@ -564,6 +916,8 @@ def run_cycle(
|
|
|
564
916
|
config: Dict[str, Any],
|
|
565
917
|
client: Any,
|
|
566
918
|
cycle_number: int,
|
|
919
|
+
reasoning_provider: str,
|
|
920
|
+
reasoning_model: str,
|
|
567
921
|
dry_run: bool = False,
|
|
568
922
|
) -> List[ActionResult]:
|
|
569
923
|
"""Run one complete agent cycle."""
|
|
@@ -588,12 +942,30 @@ def run_cycle(
|
|
|
588
942
|
# 3. Load constitution
|
|
589
943
|
constitution = load_constitution(repo_root)
|
|
590
944
|
|
|
591
|
-
# 4. Ask
|
|
945
|
+
# 4. Ask reasoning provider for a plan
|
|
592
946
|
if client is None:
|
|
593
|
-
log.info(
|
|
594
|
-
|
|
947
|
+
log.info(
|
|
948
|
+
"No API client available for provider '%s' -- skipping reasoning (dry-run)",
|
|
949
|
+
reasoning_provider,
|
|
950
|
+
)
|
|
951
|
+
plan = {
|
|
952
|
+
"reasoning": (
|
|
953
|
+
f"No API client (dry-run without provider credentials), "
|
|
954
|
+
f"provider={reasoning_provider}, active_track={active_track}"
|
|
955
|
+
),
|
|
956
|
+
"actions": [],
|
|
957
|
+
}
|
|
595
958
|
else:
|
|
596
|
-
plan =
|
|
959
|
+
plan = ask_model_for_plan(
|
|
960
|
+
client=client,
|
|
961
|
+
config=config,
|
|
962
|
+
state=state,
|
|
963
|
+
memory=memory,
|
|
964
|
+
constitution=constitution,
|
|
965
|
+
active_track=active_track,
|
|
966
|
+
provider=reasoning_provider,
|
|
967
|
+
model=reasoning_model,
|
|
968
|
+
)
|
|
597
969
|
|
|
598
970
|
actions = plan.get("actions", [])
|
|
599
971
|
if not actions:
|
|
@@ -630,8 +1002,8 @@ def run_cycle(
|
|
|
630
1002
|
action,
|
|
631
1003
|
constitution,
|
|
632
1004
|
json.dumps(memory.get("recent_decisions", [])[-5:]),
|
|
633
|
-
client=client if _HAS_ANTHROPIC else None,
|
|
634
|
-
model=
|
|
1005
|
+
client=client if reasoning_provider == "anthropic" and _HAS_ANTHROPIC else None,
|
|
1006
|
+
model=reasoning_model,
|
|
635
1007
|
)
|
|
636
1008
|
|
|
637
1009
|
log.info(
|
|
@@ -815,23 +1187,6 @@ def main() -> int:
|
|
|
815
1187
|
level = logging.DEBUG if args.verbose else logging.INFO
|
|
816
1188
|
logging.basicConfig(level=level, format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, stream=sys.stderr)
|
|
817
1189
|
|
|
818
|
-
# Check for anthropic SDK (not needed in dry-run)
|
|
819
|
-
if not _HAS_ANTHROPIC and not args.dry_run:
|
|
820
|
-
log.error(
|
|
821
|
-
"The 'anthropic' package is required. Install it:\n"
|
|
822
|
-
" pip install anthropic\n"
|
|
823
|
-
"Or: pip install -r requirements.txt"
|
|
824
|
-
)
|
|
825
|
-
return 1
|
|
826
|
-
|
|
827
|
-
# Check for API key (not needed in dry-run)
|
|
828
|
-
if not os.environ.get("ANTHROPIC_API_KEY") and not args.dry_run:
|
|
829
|
-
log.error(
|
|
830
|
-
"ANTHROPIC_API_KEY environment variable is not set.\n"
|
|
831
|
-
" export ANTHROPIC_API_KEY='your-key-here'"
|
|
832
|
-
)
|
|
833
|
-
return 1
|
|
834
|
-
|
|
835
1190
|
# Load config
|
|
836
1191
|
config_path = Path(args.config)
|
|
837
1192
|
if not config_path.exists():
|
|
@@ -839,18 +1194,89 @@ def main() -> int:
|
|
|
839
1194
|
return 1
|
|
840
1195
|
|
|
841
1196
|
config = load_config(config_path)
|
|
1197
|
+
if not isinstance(config, dict):
|
|
1198
|
+
log.error("Config file is invalid: expected a YAML mapping at %s", config_path)
|
|
1199
|
+
return 1
|
|
1200
|
+
|
|
1201
|
+
reasoning_provider = _resolve_reasoning_provider(config)
|
|
1202
|
+
reasoning_model = _resolve_reasoning_model(config, reasoning_provider)
|
|
1203
|
+
openai_runtime: Optional[Dict[str, Any]] = None
|
|
1204
|
+
openrouter_runtime: Optional[Dict[str, Any]] = None
|
|
1205
|
+
if reasoning_provider == "openai":
|
|
1206
|
+
openai_runtime = _resolve_openai_runtime(config)
|
|
1207
|
+
if reasoning_provider == "openrouter":
|
|
1208
|
+
openrouter_runtime = _resolve_openrouter_runtime(config)
|
|
1209
|
+
log.info("Reasoning provider selected: %s (model: %s)", reasoning_provider, reasoning_model)
|
|
1210
|
+
|
|
1211
|
+
# Validate provider runtime requirements (except in dry-run).
|
|
1212
|
+
if not args.dry_run:
|
|
1213
|
+
if reasoning_provider == "anthropic":
|
|
1214
|
+
if not _HAS_ANTHROPIC:
|
|
1215
|
+
log.error(
|
|
1216
|
+
"Reasoning provider is 'anthropic' but the 'anthropic' package is missing.\n"
|
|
1217
|
+
"Install it:\n"
|
|
1218
|
+
" pip install anthropic\n"
|
|
1219
|
+
"Or: pip install -r requirements.txt"
|
|
1220
|
+
)
|
|
1221
|
+
return 1
|
|
1222
|
+
if not os.environ.get("ANTHROPIC_API_KEY", "").strip():
|
|
1223
|
+
log.error(
|
|
1224
|
+
"Reasoning provider is 'anthropic' but ANTHROPIC_API_KEY is not set.\n"
|
|
1225
|
+
" export ANTHROPIC_API_KEY='your-key-here'"
|
|
1226
|
+
)
|
|
1227
|
+
return 1
|
|
1228
|
+
elif reasoning_provider == "openrouter":
|
|
1229
|
+
assert openrouter_runtime is not None
|
|
1230
|
+
if not openrouter_runtime.get("api_key"):
|
|
1231
|
+
log.error(
|
|
1232
|
+
"Reasoning provider is 'openrouter' but OPENROUTER_API_KEY is not set.\n"
|
|
1233
|
+
" export OPENROUTER_API_KEY='your-key-here'"
|
|
1234
|
+
)
|
|
1235
|
+
return 1
|
|
1236
|
+
elif reasoning_provider == "openai":
|
|
1237
|
+
assert openai_runtime is not None
|
|
1238
|
+
if not openai_runtime.get("api_key"):
|
|
1239
|
+
log.error(
|
|
1240
|
+
"Reasoning provider is 'openai' but OPENAI_API_KEY is not set.\n"
|
|
1241
|
+
" export OPENAI_API_KEY='your-key-here'"
|
|
1242
|
+
)
|
|
1243
|
+
return 1
|
|
1244
|
+
else:
|
|
1245
|
+
log.error(
|
|
1246
|
+
"Unsupported reasoning provider '%s'. Supported: anthropic, openai, openrouter",
|
|
1247
|
+
reasoning_provider,
|
|
1248
|
+
)
|
|
1249
|
+
return 1
|
|
1250
|
+
|
|
842
1251
|
log.info("Loaded config: %s v%s", config.get("agent", {}).get("name", "?"), config.get("agent", {}).get("version", "?"))
|
|
843
1252
|
|
|
844
1253
|
# Determine mode
|
|
845
1254
|
mode = args.mode or config.get("cycle", {}).get("mode", "single")
|
|
846
1255
|
|
|
847
|
-
# Initialize
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1256
|
+
# Initialize provider client.
|
|
1257
|
+
client: Any = None
|
|
1258
|
+
if reasoning_provider == "anthropic" and _HAS_ANTHROPIC and os.environ.get("ANTHROPIC_API_KEY", "").strip():
|
|
1259
|
+
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "").strip())
|
|
1260
|
+
log.info("Anthropic client initialized (model: %s)", reasoning_model)
|
|
1261
|
+
elif reasoning_provider == "openai" and openai_runtime and openai_runtime.get("api_key"):
|
|
1262
|
+
client = openai_runtime
|
|
1263
|
+
log.info(
|
|
1264
|
+
"OpenAI client initialized (model: %s, base_url: %s)",
|
|
1265
|
+
reasoning_model,
|
|
1266
|
+
openai_runtime.get("base_url"),
|
|
1267
|
+
)
|
|
1268
|
+
elif reasoning_provider == "openrouter" and openrouter_runtime and openrouter_runtime.get("api_key"):
|
|
1269
|
+
client = openrouter_runtime
|
|
1270
|
+
log.info(
|
|
1271
|
+
"OpenRouter client initialized (model: %s, base_url: %s)",
|
|
1272
|
+
reasoning_model,
|
|
1273
|
+
openrouter_runtime.get("base_url"),
|
|
1274
|
+
)
|
|
851
1275
|
else:
|
|
852
|
-
|
|
853
|
-
|
|
1276
|
+
log.info(
|
|
1277
|
+
"No provider client initialized (provider=%s, likely dry-run or missing credentials)",
|
|
1278
|
+
reasoning_provider,
|
|
1279
|
+
)
|
|
854
1280
|
|
|
855
1281
|
# Ensure memory directory exists
|
|
856
1282
|
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -871,7 +1297,14 @@ def main() -> int:
|
|
|
871
1297
|
log.info("Running single cycle (#%d)...", cycle_number)
|
|
872
1298
|
# Check PR feedback before planning
|
|
873
1299
|
check_pr_feedback(config, cycle_number)
|
|
874
|
-
results = run_cycle(
|
|
1300
|
+
results = run_cycle(
|
|
1301
|
+
config=config,
|
|
1302
|
+
client=client,
|
|
1303
|
+
cycle_number=cycle_number,
|
|
1304
|
+
reasoning_provider=reasoning_provider,
|
|
1305
|
+
reasoning_model=reasoning_model,
|
|
1306
|
+
dry_run=args.dry_run,
|
|
1307
|
+
)
|
|
875
1308
|
succeeded = sum(1 for r in results if r.success)
|
|
876
1309
|
failed = sum(1 for r in results if not r.success)
|
|
877
1310
|
log.info("Cycle %d complete: %d succeeded, %d failed", cycle_number, succeeded, failed)
|
|
@@ -886,7 +1319,14 @@ def main() -> int:
|
|
|
886
1319
|
cycles_run = 0
|
|
887
1320
|
while True:
|
|
888
1321
|
check_pr_feedback(config, cycle_number)
|
|
889
|
-
results = run_cycle(
|
|
1322
|
+
results = run_cycle(
|
|
1323
|
+
config=config,
|
|
1324
|
+
client=client,
|
|
1325
|
+
cycle_number=cycle_number,
|
|
1326
|
+
reasoning_provider=reasoning_provider,
|
|
1327
|
+
reasoning_model=reasoning_model,
|
|
1328
|
+
dry_run=args.dry_run,
|
|
1329
|
+
)
|
|
890
1330
|
succeeded = sum(1 for r in results if r.success)
|
|
891
1331
|
failed = sum(1 for r in results if not r.success)
|
|
892
1332
|
log.info("Cycle %d complete: %d succeeded, %d failed", cycle_number, succeeded, failed)
|