Compare commits
1 Commits
burn/372-1
...
burn/373-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c735936f24 |
@@ -544,16 +544,8 @@ def _run_job_script(script_path: str) -> tuple[bool, str]:
|
||||
return False, f"Script execution failed: {exc}"
|
||||
|
||||
|
||||
def _build_job_prompt(job: dict, *, runtime_model: str = "", runtime_provider: str = "") -> str:
|
||||
"""Build the effective prompt for a cron job, optionally loading one or more skills first.
|
||||
|
||||
Args:
|
||||
job: The cron job dict.
|
||||
runtime_model: The resolved model name (e.g. "xiaomi/mimo-v2-pro").
|
||||
If provided, injected into the cron hint so the agent knows its own
|
||||
capabilities and can avoid prompts that assume local/Ollama access.
|
||||
runtime_provider: The resolved provider name (e.g. "nous", "openrouter").
|
||||
"""
|
||||
def _build_job_prompt(job: dict) -> str:
|
||||
"""Build the effective prompt for a cron job, optionally loading one or more skills first."""
|
||||
prompt = job.get("prompt", "")
|
||||
skills = job.get("skills")
|
||||
|
||||
@@ -604,43 +596,6 @@ def _build_job_prompt(job: dict, *, runtime_model: str = "", runtime_provider: s
|
||||
"\"[SCRIPT_FAILED]: forge.alexanderwhitestone.com timed out\" "
|
||||
"\"[SCRIPT_FAILED]: script exited with code 1\".]\\n\\n"
|
||||
)
|
||||
|
||||
# Inject runtime context so the agent knows its own capabilities.
|
||||
# This prevents prompts from assuming local Ollama/SSH when running
|
||||
# on a cloud API provider (e.g. nous/mimo-v2-pro).
|
||||
if runtime_model or runtime_provider:
|
||||
is_local = (
|
||||
runtime_provider in ("ollama", "local", "")
|
||||
or "ollama" in (runtime_model or "").lower()
|
||||
)
|
||||
is_cloud = not is_local and bool(runtime_provider)
|
||||
has_terminal = True # Cron jobs always have terminal tool
|
||||
|
||||
capability_notes = []
|
||||
if runtime_model:
|
||||
capability_notes.append(f"MODEL: {runtime_model}")
|
||||
if runtime_provider:
|
||||
capability_notes.append(f"PROVIDER: {runtime_provider}")
|
||||
if is_local:
|
||||
capability_notes.append(
|
||||
"RUNTIME: local — you have access to the local machine, "
|
||||
"local Ollama, SSH keys, and filesystem"
|
||||
)
|
||||
elif is_cloud:
|
||||
capability_notes.append(
|
||||
"RUNTIME: cloud API — you do NOT have local machine access. "
|
||||
"Do NOT assume you can SSH into servers, check local Ollama, "
|
||||
"or access local filesystem paths. Use terminal tools only "
|
||||
"for commands that work from this environment."
|
||||
)
|
||||
|
||||
if capability_notes:
|
||||
runtime_ctx = (
|
||||
"[SYSTEM: RUNTIME CONTEXT — "
|
||||
+ "; ".join(capability_notes)
|
||||
+ ". Adjust your approach based on these capabilities.]\\n\\n"
|
||||
)
|
||||
cron_hint = runtime_ctx + cron_hint
|
||||
prompt = cron_hint + prompt
|
||||
if skills is None:
|
||||
legacy = job.get("skill")
|
||||
@@ -711,31 +666,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
|
||||
|
||||
job_id = job["id"]
|
||||
job_name = job["name"]
|
||||
|
||||
# Resolve model early so we can inject capability context into the prompt.
|
||||
# The full provider resolution happens later (smart routing, etc.) but we
|
||||
# need the basic model/provider name for the runtime context hint.
|
||||
_early_model = job.get("model") or os.getenv("HERMES_MODEL") or ""
|
||||
_early_provider = os.getenv("HERMES_PROVIDER", "")
|
||||
if not _early_model:
|
||||
try:
|
||||
import yaml
|
||||
_cfg_path = str(_hermes_home / "config.yaml")
|
||||
if os.path.exists(_cfg_path):
|
||||
with open(_cfg_path) as _f:
|
||||
_cfg_early = yaml.safe_load(_f) or {}
|
||||
_mc = _cfg_early.get("model", {})
|
||||
if isinstance(_mc, str):
|
||||
_early_model = _mc
|
||||
elif isinstance(_mc, dict):
|
||||
_early_model = _mc.get("default", "")
|
||||
except Exception:
|
||||
pass
|
||||
# Derive provider from model prefix if not explicitly set
|
||||
if not _early_provider and "/" in _early_model:
|
||||
_early_provider = _early_model.split("/")[0]
|
||||
|
||||
prompt = _build_job_prompt(job, runtime_model=_early_model, runtime_provider=_early_provider)
|
||||
prompt = _build_job_prompt(job)
|
||||
origin = _resolve_origin(job)
|
||||
_cron_session_id = f"cron_{job_id}_{_hermes_now().strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
|
||||
@@ -1433,18 +1433,27 @@ def validate_config_structure(config: Optional[Dict[str, Any]] = None) -> List["
|
||||
" model: anthropic/claude-sonnet-4",
|
||||
))
|
||||
elif fb:
|
||||
if not fb.get("provider"):
|
||||
issues.append(ConfigIssue(
|
||||
"warning",
|
||||
"fallback_model is missing 'provider' field — fallback will be disabled",
|
||||
"Add: provider: openrouter (or another provider)",
|
||||
))
|
||||
if not fb.get("model"):
|
||||
issues.append(ConfigIssue(
|
||||
"warning",
|
||||
"fallback_model is missing 'model' field — fallback will be disabled",
|
||||
"Add: model: anthropic/claude-sonnet-4 (or another model)",
|
||||
))
|
||||
# If both provider and model are blank/empty, treat as intentionally disabled
|
||||
provider = fb.get("provider")
|
||||
model = fb.get("model")
|
||||
provider_blank = not provider or (isinstance(provider, str) and not provider.strip())
|
||||
model_blank = not model or (isinstance(model, str) and not model.strip())
|
||||
|
||||
# Only warn if at least one field is set (user might be trying to configure)
|
||||
# If both are blank, treat as intentionally disabled
|
||||
if not provider_blank or not model_blank:
|
||||
if provider_blank:
|
||||
issues.append(ConfigIssue(
|
||||
"warning",
|
||||
"fallback_model is missing 'provider' field — fallback will be disabled",
|
||||
"Add: provider: openrouter (or another provider), or set both fields blank to disable",
|
||||
))
|
||||
if model_blank:
|
||||
issues.append(ConfigIssue(
|
||||
"warning",
|
||||
"fallback_model is missing 'model' field — fallback will be disabled",
|
||||
"Add: model: anthropic/claude-sonnet-4 (or another model), or set both fields blank to disable",
|
||||
))
|
||||
|
||||
# ── Check for fallback_model accidentally nested inside custom_providers ──
|
||||
if isinstance(cp, dict) and "fallback_model" not in config and "fallback_model" in (cp or {}):
|
||||
|
||||
@@ -136,6 +136,51 @@ class TestFallbackModelValidation:
|
||||
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
|
||||
assert len(fb_issues) == 0
|
||||
|
||||
def test_blank_fallback_fields_no_issues(self):
|
||||
"""Blank fallback_model fields (both empty) should not trigger warnings."""
|
||||
issues = validate_config_structure({
|
||||
"fallback_model": {
|
||||
"provider": "",
|
||||
"model": "",
|
||||
},
|
||||
})
|
||||
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
|
||||
assert len(fb_issues) == 0
|
||||
|
||||
def test_blank_fallback_fields_with_whitespace_no_issues(self):
|
||||
"""Blank fallback_model fields with whitespace should not trigger warnings."""
|
||||
issues = validate_config_structure({
|
||||
"fallback_model": {
|
||||
"provider": " ",
|
||||
"model": " ",
|
||||
},
|
||||
})
|
||||
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
|
||||
assert len(fb_issues) == 0
|
||||
|
||||
def test_none_fallback_fields_no_issues(self):
|
||||
"""None fallback_model fields should not trigger warnings."""
|
||||
issues = validate_config_structure({
|
||||
"fallback_model": {
|
||||
"provider": None,
|
||||
"model": None,
|
||||
},
|
||||
})
|
||||
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
|
||||
assert len(fb_issues) == 0
|
||||
|
||||
def test_partial_blank_fallback_warns(self):
|
||||
"""Partial blank fallback (only one field blank) should warn."""
|
||||
issues = validate_config_structure({
|
||||
"fallback_model": {
|
||||
"provider": "",
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
},
|
||||
})
|
||||
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
|
||||
assert len(fb_issues) == 1
|
||||
assert "provider" in fb_issues[0].message
|
||||
|
||||
|
||||
class TestMissingModelSection:
|
||||
"""Warn when custom_providers exists but model section is missing."""
|
||||
|
||||
Reference in New Issue
Block a user