Merge pull request #109 from open-jarvis/fix/agent-config-defaults-103

fix: agent constructor defaults resolve from config.toml
This commit is contained in:
Jon Saad-Falcon
2026-03-24 16:54:44 -07:00
committed by GitHub
11 changed files with 533 additions and 210 deletions
+54 -12
View File
@@ -13,6 +13,7 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from openjarvis.core.config import load_config
from openjarvis.core.events import EventBus, EventType
from openjarvis.core.types import Conversation, Message, Role, ToolResult
from openjarvis.engine._stubs import InferenceEngine
@@ -63,17 +64,44 @@ class BaseAgent(ABC):
model: str,
*,
bus: Optional[EventBus] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
prompt_builder: Optional[Any] = None,
) -> None:
self._engine = engine
self._model = model
self._bus = bus
self._temperature = temperature
self._max_tokens = max_tokens
self._prompt_builder = prompt_builder
# Three-tier resolution: explicit arg > config > class default > hardcoded
if temperature is not None and max_tokens is not None:
self._temperature = temperature
self._max_tokens = max_tokens
else:
try:
cfg = load_config()
self._temperature = (
temperature
if temperature is not None
else cfg.intelligence.temperature
)
self._max_tokens = (
max_tokens
if max_tokens is not None
else cfg.intelligence.max_tokens
)
except Exception:
self._temperature = (
temperature
if temperature is not None
else getattr(self, "_default_temperature", 0.7)
)
self._max_tokens = (
max_tokens
if max_tokens is not None
else getattr(self, "_default_max_tokens", 1024)
)
# ------------------------------------------------------------------
# Concrete helpers
# ------------------------------------------------------------------
@@ -197,7 +225,9 @@ class BaseAgent(ABC):
"""
# Full <think>...</think> blocks
text = re.sub(
r"<think>.*?</think>\s*", "", text,
r"<think>.*?</think>\s*",
"",
text,
flags=re.DOTALL | re.IGNORECASE,
)
# Leading content before a bare </think> (no opening tag)
@@ -230,9 +260,9 @@ class ToolUsingAgent(BaseAgent):
*,
tools: Optional[List["BaseTool"]] = None, # noqa: F821
bus: Optional[EventBus] = None,
max_turns: int = 10,
temperature: float = 0.7,
max_tokens: int = 1024,
max_turns: Optional[int] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
loop_guard_config: Optional[Any] = None,
capability_policy: Optional[Any] = None,
agent_id: Optional[str] = None,
@@ -240,21 +270,33 @@ class ToolUsingAgent(BaseAgent):
confirm_callback: Optional[Any] = None,
) -> None:
super().__init__(
engine, model, bus=bus,
temperature=temperature, max_tokens=max_tokens,
engine,
model,
bus=bus,
temperature=temperature,
max_tokens=max_tokens,
)
from openjarvis.tools._stubs import ToolExecutor
self._tools = tools or []
_aid = agent_id or getattr(self, "agent_id", "")
self._executor = ToolExecutor(
self._tools, bus=bus,
self._tools,
bus=bus,
capability_policy=capability_policy,
agent_id=_aid,
interactive=interactive,
confirm_callback=confirm_callback,
)
self._max_turns = max_turns
# Resolve max_turns: explicit arg > config > class default > 10
if max_turns is not None:
self._max_turns = max_turns
else:
try:
cfg = load_config()
self._max_turns = cfg.agent.max_turns
except Exception:
self._max_turns = getattr(self, "_default_max_turns", 10)
# Loop guard
self._loop_guard = None
+12 -6
View File
@@ -55,6 +55,8 @@ class ClaudeCodeAgent(BaseAgent):
agent_id = "claude_code"
accepts_tools = False
_default_temperature = 0.7
_default_max_tokens = 1024
def __init__(
self,
@@ -62,8 +64,8 @@ class ClaudeCodeAgent(BaseAgent):
model: str,
*,
bus: Optional[EventBus] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
api_key: str = "",
workspace: str = "",
session_id: str = "",
@@ -72,8 +74,11 @@ class ClaudeCodeAgent(BaseAgent):
timeout: int = 300,
) -> None:
super().__init__(
engine, model, bus=bus,
temperature=temperature, max_tokens=max_tokens,
engine,
model,
bus=bus,
temperature=temperature,
max_tokens=max_tokens,
)
self._api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
self._workspace = workspace or os.getcwd()
@@ -178,7 +183,8 @@ class ClaudeCodeAgent(BaseAgent):
stderr = proc.stderr.strip() if proc.stderr else "Unknown error"
logger.error(
"claude_code_runner exited with code %d: %s",
proc.returncode, stderr,
proc.returncode,
stderr,
)
self._emit_turn_end(turns=1, error=True)
return AgentResult(
@@ -217,7 +223,7 @@ class ClaudeCodeAgent(BaseAgent):
# No sentinels -- treat entire stdout as plain content
return stdout.strip(), [], {}
json_str = stdout[start + len(_OUTPUT_START):end].strip()
json_str = stdout[start + len(_OUTPUT_START) : end].strip()
try:
data = json.loads(json_str)
+57 -32
View File
@@ -84,6 +84,9 @@ class MonitorOperativeAgent(ToolUsingAgent):
agent_id = "monitor_operative"
accepts_tools = True
_default_temperature = 0.3
_default_max_tokens = 4096
_default_max_turns = 25
def __init__(
self,
@@ -92,9 +95,9 @@ class MonitorOperativeAgent(ToolUsingAgent):
*,
tools: Optional[List[BaseTool]] = None,
bus: Optional[EventBus] = None,
max_turns: int = 25,
temperature: float = 0.3,
max_tokens: int = 4096,
max_turns: Optional[int] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None,
# Strategy parameters
memory_extraction: str = "causality_graph",
@@ -110,10 +113,15 @@ class MonitorOperativeAgent(ToolUsingAgent):
**kwargs: Any,
) -> None:
super().__init__(
engine, model, tools=tools, bus=bus,
max_turns=max_turns, temperature=temperature,
engine,
model,
tools=tools,
bus=bus,
max_turns=max_turns,
temperature=temperature,
max_tokens=max_tokens,
interactive=interactive, confirm_callback=confirm_callback,
interactive=interactive,
confirm_callback=confirm_callback,
)
# Validate strategies
if memory_extraction not in VALID_MEMORY_EXTRACTION:
@@ -191,7 +199,8 @@ class MonitorOperativeAgent(ToolUsingAgent):
# 4. Build messages
messages = self._build_operative_messages(
input, context,
input,
context,
system_prompt=system_prompt,
session_messages=session_messages,
)
@@ -238,18 +247,21 @@ class MonitorOperativeAgent(ToolUsingAgent):
]
# Append assistant message with tool calls
messages.append(Message(
role=Role.ASSISTANT,
content=content,
tool_calls=tool_calls,
))
messages.append(
Message(
role=Role.ASSISTANT,
content=content,
tool_calls=tool_calls,
)
)
# Execute each tool
for tc in tool_calls:
# Loop guard check
if self._loop_guard:
verdict = self._loop_guard.check_call(
tc.name, tc.arguments,
tc.name,
tc.arguments,
)
if verdict.blocked:
tool_result = ToolResult(
@@ -258,12 +270,14 @@ class MonitorOperativeAgent(ToolUsingAgent):
success=False,
)
all_tool_results.append(tool_result)
messages.append(Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
))
messages.append(
Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
)
)
continue
tool_result = self._executor.execute(tc)
@@ -279,18 +293,21 @@ class MonitorOperativeAgent(ToolUsingAgent):
except (json.JSONDecodeError, TypeError) as exc:
logger.debug(
"Failed to parse tool call arguments"
" for state tracking: %s", exc,
" for state tracking: %s",
exc,
)
# Compress observation if strategy requires it
observation_content = self._compress_observation(tool_result.content)
messages.append(Message(
role=Role.TOOL,
content=observation_content,
tool_call_id=tc.id,
name=tc.name,
))
messages.append(
Message(
role=Role.TOOL,
content=observation_content,
tool_call_id=tc.id,
name=tc.name,
)
)
# Extract and store findings based on memory strategy
self._extract_and_store(tc.name, tool_result.content)
@@ -298,7 +315,9 @@ class MonitorOperativeAgent(ToolUsingAgent):
# Max turns exceeded
self._save_session(input, content)
return self._max_turns_result(
all_tool_results, turns, content=content,
all_tool_results,
turns,
content=content,
metadata=total_usage,
)
@@ -345,6 +364,7 @@ class MonitorOperativeAgent(ToolUsingAgent):
if not self._tools:
return ""
from openjarvis.tools._stubs import build_tool_descriptions
return build_tool_descriptions(self._tools)
# ------------------------------------------------------------------
@@ -458,11 +478,13 @@ class MonitorOperativeAgent(ToolUsingAgent):
self._memory_backend.store(key, value)
except Exception as exc:
logger.debug(
"Failed to store causality relation in memory: %s", exc,
"Failed to store causality relation in memory: %s",
exc,
)
except (json.JSONDecodeError, Exception):
logger.debug(
"Causality extraction failed for tool %s output", tool_name,
"Causality extraction failed for tool %s output",
tool_name,
)
def _store_scratchpad(self, tool_name: str, content: str) -> None:
@@ -503,7 +525,8 @@ class MonitorOperativeAgent(ToolUsingAgent):
except Exception as exc:
logger.debug(
"Failed to store structured data for tool %s: %s",
tool_name, exc,
tool_name,
exc,
)
# ------------------------------------------------------------------
@@ -557,10 +580,12 @@ class MonitorOperativeAgent(ToolUsingAgent):
session_id = f"monitor_operative:{self._operator_id}"
try:
self._session_store.save_message(
session_id, {"role": "user", "content": input_text},
session_id,
{"role": "user", "content": input_text},
)
self._session_store.save_message(
session_id, {"role": "assistant", "content": response},
session_id,
{"role": "assistant", "content": response},
)
except Exception:
logger.debug(
+26 -19
View File
@@ -54,6 +54,9 @@ class NativeOpenHandsAgent(ToolUsingAgent):
"""Native CodeAct agent -- generates and executes Python code."""
agent_id = "native_openhands"
_default_temperature = 0.7
_default_max_tokens = 2048
_default_max_turns = 3
def __init__(
self,
@@ -62,17 +65,22 @@ class NativeOpenHandsAgent(ToolUsingAgent):
*,
tools: Optional[List[BaseTool]] = None,
bus: Optional[EventBus] = None,
max_turns: int = 3,
temperature: float = 0.7,
max_tokens: int = 2048,
max_turns: Optional[int] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
interactive: bool = False,
confirm_callback=None,
) -> None:
super().__init__(
engine, model, tools=tools, bus=bus,
max_turns=max_turns, temperature=temperature,
engine,
model,
tools=tools,
bus=bus,
max_turns=max_turns,
temperature=temperature,
max_tokens=max_tokens,
interactive=interactive, confirm_callback=confirm_callback,
interactive=interactive,
confirm_callback=confirm_callback,
)
@staticmethod
@@ -93,9 +101,7 @@ class NativeOpenHandsAgent(ToolUsingAgent):
content = WebSearchTool._fetch_url(url, max_chars=4000)
header = f"\n\n--- Content from {url} ---\n"
footer = "\n--- End of content ---\n"
expanded = text.replace(
url, f"{header}{content}{footer}"
)
expanded = text.replace(url, f"{header}{content}{footer}")
return expanded, True
except Exception:
return text, False
@@ -121,9 +127,7 @@ class NativeOpenHandsAgent(ToolUsingAgent):
messages[i] = Message(
role=Role.USER,
content=(
truncated
+ "\n\n[Input truncated"
" to fit context window]"
truncated + "\n\n[Input truncated to fit context window]"
),
)
break
@@ -135,7 +139,9 @@ class NativeOpenHandsAgent(ToolUsingAgent):
# Remove Action: ... Action Input: ... blocks
text = re.sub(
r"Action:\s*.+?(?:Action Input:\s*.+?)?(?=\n\n|\Z)",
"", text, flags=re.DOTALL | re.IGNORECASE,
"",
text,
flags=re.DOTALL | re.IGNORECASE,
)
# Remove <tool_call>...</tool_call> or </tool_name> blocks
text = re.sub(r"<tool_call>.*?</\w+>", "", text, flags=re.DOTALL)
@@ -242,7 +248,9 @@ class NativeOpenHandsAgent(ToolUsingAgent):
usage = result.get("usage", {})
self._emit_turn_end(turns=1)
return AgentResult(
content=content, tool_results=[], turns=1,
content=content,
tool_results=[],
turns=1,
metadata={
"prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0),
@@ -258,10 +266,7 @@ class NativeOpenHandsAgent(ToolUsingAgent):
"Please try a shorter message."
)
else:
error_msg = (
"The model returned an error: "
+ error_str
)
error_msg = "The model returned an error: " + error_str
self._emit_turn_end(turns=1, error=True)
return AgentResult(
content=error_msg,
@@ -358,7 +363,9 @@ class NativeOpenHandsAgent(ToolUsingAgent):
content = self._strip_tool_call_text(content)
self._emit_turn_end(turns=turns)
return AgentResult(
content=content, tool_results=all_tool_results, turns=turns,
content=content,
tool_results=all_tool_results,
turns=turns,
metadata=total_usage,
)
+16 -7
View File
@@ -36,6 +36,9 @@ class NativeReActAgent(ToolUsingAgent):
"""ReAct agent: Thought -> Action -> Observation loop."""
agent_id = "native_react"
_default_temperature = 0.7
_default_max_tokens = 1024
_default_max_turns = 10
def __init__(
self,
@@ -44,17 +47,22 @@ class NativeReActAgent(ToolUsingAgent):
*,
tools: Optional[List[BaseTool]] = None,
bus: Optional[EventBus] = None,
max_turns: int = 10,
temperature: float = 0.7,
max_tokens: int = 1024,
max_turns: Optional[int] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
interactive: bool = False,
confirm_callback=None,
) -> None:
super().__init__(
engine, model, tools=tools, bus=bus,
max_turns=max_turns, temperature=temperature,
engine,
model,
tools=tools,
bus=bus,
max_turns=max_turns,
temperature=temperature,
max_tokens=max_tokens,
interactive=interactive, confirm_callback=confirm_callback,
interactive=interactive,
confirm_callback=confirm_callback,
)
def _parse_response(self, text: str) -> dict:
@@ -161,7 +169,8 @@ class NativeReActAgent(ToolUsingAgent):
# Loop guard check before execution
if self._loop_guard:
verdict = self._loop_guard.check_call(
tool_call.name, tool_call.arguments,
tool_call.name,
tool_call.arguments,
)
if verdict.blocked:
tool_result = ToolResult(
+9 -4
View File
@@ -25,6 +25,8 @@ class OpenHandsAgent(BaseAgent):
"""
agent_id = "openhands"
_default_temperature = 0.7
_default_max_tokens = 1024
def __init__(
self,
@@ -32,14 +34,17 @@ class OpenHandsAgent(BaseAgent):
model: str,
*,
bus: Optional[EventBus] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
workspace: Optional[str] = None,
api_key: Optional[str] = None,
) -> None:
super().__init__(
engine, model, bus=bus,
temperature=temperature, max_tokens=max_tokens,
engine,
model,
bus=bus,
temperature=temperature,
max_tokens=max_tokens,
)
self._workspace = workspace or os.getcwd()
self._api_key = api_key or os.environ.get("LLM_API_KEY", "")
+44 -26
View File
@@ -38,6 +38,9 @@ class OperativeAgent(ToolUsingAgent):
agent_id = "operative"
accepts_tools = True
_default_temperature = 0.3
_default_max_tokens = 2048
_default_max_turns = 20
def __init__(
self,
@@ -46,9 +49,9 @@ class OperativeAgent(ToolUsingAgent):
*,
tools: Optional[List[BaseTool]] = None,
bus: Optional[EventBus] = None,
max_turns: int = 20,
temperature: float = 0.3,
max_tokens: int = 2048,
max_turns: Optional[int] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None,
operator_id: Optional[str] = None,
session_store: Optional[Any] = None,
@@ -58,10 +61,15 @@ class OperativeAgent(ToolUsingAgent):
**kwargs: Any,
) -> None:
super().__init__(
engine, model, tools=tools, bus=bus,
max_turns=max_turns, temperature=temperature,
engine,
model,
tools=tools,
bus=bus,
max_turns=max_turns,
temperature=temperature,
max_tokens=max_tokens,
interactive=interactive, confirm_callback=confirm_callback,
interactive=interactive,
confirm_callback=confirm_callback,
)
self._system_prompt = system_prompt or ""
self._operator_id = operator_id
@@ -94,7 +102,9 @@ class OperativeAgent(ToolUsingAgent):
# 4. Build messages
messages = self._build_operative_messages(
input, context, system_prompt=system_prompt,
input,
context,
system_prompt=system_prompt,
session_messages=session_messages,
)
@@ -140,11 +150,13 @@ class OperativeAgent(ToolUsingAgent):
for i, tc in enumerate(raw_tool_calls)
]
messages.append(Message(
role=Role.ASSISTANT,
content=content,
tool_calls=tool_calls,
))
messages.append(
Message(
role=Role.ASSISTANT,
content=content,
tool_calls=tool_calls,
)
)
for tc in tool_calls:
# Loop guard check
@@ -157,12 +169,14 @@ class OperativeAgent(ToolUsingAgent):
success=False,
)
all_tool_results.append(tool_result)
messages.append(Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
))
messages.append(
Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
)
)
continue
tool_result = self._executor.execute(tc)
@@ -178,12 +192,14 @@ class OperativeAgent(ToolUsingAgent):
except (json.JSONDecodeError, TypeError):
pass
messages.append(Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
))
messages.append(
Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
)
)
else:
# Max turns exceeded
self._save_session(input, content)
@@ -274,10 +290,12 @@ class OperativeAgent(ToolUsingAgent):
session_id = f"operator:{self._operator_id}"
try:
self._session_store.save_message(
session_id, {"role": "user", "content": input_text},
session_id,
{"role": "user", "content": input_text},
)
self._session_store.save_message(
session_id, {"role": "assistant", "content": response},
session_id,
{"role": "assistant", "content": response},
)
except Exception:
logger.debug("Could not save session for operator %s", self._operator_id)
+55 -47
View File
@@ -41,6 +41,9 @@ class OrchestratorAgent(ToolUsingAgent):
"""
agent_id = "orchestrator"
_default_temperature = 0.7
_default_max_tokens = 1024
_default_max_turns = 10
def __init__(
self,
@@ -49,9 +52,9 @@ class OrchestratorAgent(ToolUsingAgent):
*,
tools: Optional[List[BaseTool]] = None,
bus: Optional[EventBus] = None,
max_turns: int = 10,
temperature: float = 0.7,
max_tokens: int = 1024,
max_turns: Optional[int] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
mode: str = "function_calling",
system_prompt: Optional[str] = None,
parallel_tools: bool = True,
@@ -59,10 +62,15 @@ class OrchestratorAgent(ToolUsingAgent):
confirm_callback=None,
) -> None:
super().__init__(
engine, model, tools=tools, bus=bus,
max_turns=max_turns, temperature=temperature,
engine,
model,
tools=tools,
bus=bus,
max_turns=max_turns,
temperature=temperature,
max_tokens=max_tokens,
interactive=interactive, confirm_callback=confirm_callback,
interactive=interactive,
confirm_callback=confirm_callback,
)
self._mode = mode
self._system_prompt = system_prompt
@@ -97,6 +105,7 @@ class OrchestratorAgent(ToolUsingAgent):
from openjarvis.learning.intelligence.orchestrator.prompt_registry import (
build_system_prompt,
)
sys_prompt = build_system_prompt(tools=self._tools)
messages = self._build_messages(input, context, system_prompt=sys_prompt)
@@ -126,9 +135,7 @@ class OrchestratorAgent(ToolUsingAgent):
# TOOL -> execute
if parsed["tool"]:
messages.append(
Message(role=Role.ASSISTANT, content=content)
)
messages.append(Message(role=Role.ASSISTANT, content=content))
tool_call = ToolCall(
id=f"orch_{turns}",
@@ -138,12 +145,8 @@ class OrchestratorAgent(ToolUsingAgent):
tool_result = self._executor.execute(tool_call)
all_tool_results.append(tool_result)
observation = (
f"Observation: {tool_result.content}"
)
messages.append(
Message(role=Role.USER, content=observation)
)
observation = f"Observation: {tool_result.content}"
messages.append(Message(role=Role.USER, content=observation))
continue
# Neither -> treat content as final answer
@@ -184,9 +187,7 @@ class OrchestratorAgent(ToolUsingAgent):
result["final_answer"] = final_match.group(1).strip()
return result
tool_match = re.search(
r"TOOL:\s*(.+)", text, re.IGNORECASE
)
tool_match = re.search(r"TOOL:\s*(.+)", text, re.IGNORECASE)
if tool_match:
result["tool"] = tool_match.group(1).strip()
@@ -271,11 +272,13 @@ class OrchestratorAgent(ToolUsingAgent):
]
# Append assistant message with tool calls
messages.append(Message(
role=Role.ASSISTANT,
content=content,
tool_calls=tool_calls,
))
messages.append(
Message(
role=Role.ASSISTANT,
content=content,
tool_calls=tool_calls,
)
)
# Execute each tool (with loop guard check) and append results
if self._parallel_tools and len(tool_calls) > 1:
@@ -283,7 +286,8 @@ class OrchestratorAgent(ToolUsingAgent):
def _exec_tool(tc: ToolCall) -> tuple:
if self._loop_guard:
verdict = self._loop_guard.check_call(
tc.name, tc.arguments,
tc.name,
tc.arguments,
)
if verdict.blocked:
return tc, ToolResult(
@@ -296,10 +300,7 @@ class OrchestratorAgent(ToolUsingAgent):
with concurrent.futures.ThreadPoolExecutor(
max_workers=len(tool_calls),
) as pool:
futures = {
pool.submit(_exec_tool, tc): tc
for tc in tool_calls
}
futures = {pool.submit(_exec_tool, tc): tc for tc in tool_calls}
results_map: dict[int, tuple] = {}
for future in concurrent.futures.as_completed(futures):
tc_orig = futures[future]
@@ -309,19 +310,22 @@ class OrchestratorAgent(ToolUsingAgent):
for tc in tool_calls:
_, tool_result = results_map[id(tc)]
all_tool_results.append(tool_result)
messages.append(Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
))
messages.append(
Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
)
)
else:
# Sequential execution
for tc in tool_calls:
# Loop guard check before execution
if self._loop_guard:
verdict = self._loop_guard.check_call(
tc.name, tc.arguments,
tc.name,
tc.arguments,
)
if verdict.blocked:
tool_result = ToolResult(
@@ -330,24 +334,28 @@ class OrchestratorAgent(ToolUsingAgent):
success=False,
)
all_tool_results.append(tool_result)
messages.append(Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
))
messages.append(
Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
)
)
continue
tool_result = self._executor.execute(tc)
all_tool_results.append(tool_result)
# Append tool response message
messages.append(Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
))
messages.append(
Message(
role=Role.TOOL,
content=tool_result.content,
tool_call_id=tc.id,
name=tc.name,
)
)
# Max turns exceeded
final_content = self._strip_think_tags(content) if content else ""
+36 -24
View File
@@ -35,8 +35,8 @@ RLM_SYSTEM_PROMPT = (
"final answer.\n"
"- `FINAL_VAR(var_name: str)` — Terminate and return the "
"value of variable `var_name`.\n"
"- `answer` dict — Set `answer[\"value\"] = ...` and "
"`answer[\"ready\"] = True` to terminate.\n\n"
'- `answer` dict — Set `answer["value"] = ...` and '
'`answer["ready"] = True` to terminate.\n\n'
"{tool_section}"
"## Available Modules\n\n"
"json, re, math, collections, itertools, functools, "
@@ -52,7 +52,7 @@ RLM_SYSTEM_PROMPT = (
"and use `llm_query()` on each chunk.\n"
"3. Combine sub-results programmatically.\n"
"4. When you have the final answer, call "
"`FINAL(answer_value)` or `FINAL_VAR(\"var_name\")`.\n"
'`FINAL(answer_value)` or `FINAL_VAR("var_name")`.\n'
"5. If you can answer directly without code, just respond "
"with text (no code block).\n\n"
"## Strategy Tips\n\n"
@@ -84,6 +84,9 @@ class RLMAgent(ToolUsingAgent):
"""
agent_id = "rlm"
_default_temperature = 0.7
_default_max_tokens = 2048
_default_max_turns = 10
def __init__(
self,
@@ -92,9 +95,9 @@ class RLMAgent(ToolUsingAgent):
*,
tools: Optional[List[BaseTool]] = None,
bus: Optional[EventBus] = None,
max_turns: int = 10,
temperature: float = 0.7,
max_tokens: int = 2048,
max_turns: Optional[int] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
sub_model: Optional[str] = None,
sub_temperature: float = 0.3,
sub_max_tokens: int = 1024,
@@ -104,10 +107,15 @@ class RLMAgent(ToolUsingAgent):
confirm_callback=None,
) -> None:
super().__init__(
engine, model, tools=tools, bus=bus,
max_turns=max_turns, temperature=temperature,
engine,
model,
tools=tools,
bus=bus,
max_turns=max_turns,
temperature=temperature,
max_tokens=max_tokens,
interactive=interactive, confirm_callback=confirm_callback,
interactive=interactive,
confirm_callback=confirm_callback,
)
# Override executor: RLM only creates one if tools are provided
if not self._tools:
@@ -168,7 +176,9 @@ class RLMAgent(ToolUsingAgent):
# Build conversation
messages = self._build_messages(
input, context, system_prompt=system_prompt,
input,
context,
system_prompt=system_prompt,
)
all_tool_results: list[ToolResult] = []
@@ -233,9 +243,7 @@ class RLMAgent(ToolUsingAgent):
# Feed output back as user message
messages.append(Message(role=Role.ASSISTANT, content=content))
feedback = (
f"REPL Output: {output}"
if output
else "REPL Output: (no output)"
f"REPL Output: {output}" if output else "REPL Output: (no output)"
)
messages.append(Message(role=Role.USER, content=feedback))
@@ -281,19 +289,23 @@ class RLMAgent(ToolUsingAgent):
)
for i, tc in enumerate(raw_tool_calls)
]
messages.append(Message(
role=Role.ASSISTANT,
content=content,
tool_calls=tool_calls,
))
messages.append(
Message(
role=Role.ASSISTANT,
content=content,
tool_calls=tool_calls,
)
)
for tc in tool_calls:
tr = self._executor.execute(tc)
messages.append(Message(
role=Role.TOOL,
content=tr.content,
tool_call_id=tc.id,
name=tc.name,
))
messages.append(
Message(
role=Role.TOOL,
content=tr.content,
tool_call_id=tc.id,
name=tc.name,
)
)
followup = self._engine.generate(
messages,
model=self._sub_model,
+53 -33
View File
@@ -7,6 +7,7 @@ found in the TOML file.
from __future__ import annotations
import functools
import os
import platform
import shutil
@@ -59,7 +60,10 @@ def _run_cmd(cmd: list[str]) -> str:
"""Run a command and return stripped stdout, or empty string on failure."""
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=10, # noqa: S603
cmd,
capture_output=True,
text=True,
timeout=10, # noqa: S603
)
return result.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
@@ -69,11 +73,13 @@ def _run_cmd(cmd: list[str]) -> str:
def _detect_nvidia_gpu() -> Optional[GpuInfo]:
if not shutil.which("nvidia-smi"):
return None
raw = _run_cmd([
"nvidia-smi",
"--query-gpu=name,memory.total,count",
"--format=csv,noheader,nounits",
])
raw = _run_cmd(
[
"nvidia-smi",
"--query-gpu=name,memory.total,count",
"--format=csv,noheader,nounits",
]
)
if not raw:
return None
try:
@@ -117,6 +123,7 @@ def _detect_amd_gpu() -> Optional[GpuInfo]:
try:
allinfo_raw = _run_cmd(["rocm-smi", "--showallinfo"])
import re
gpu_ids = set(re.findall(r"GPU\[(\d+)\]", allinfo_raw))
if gpu_ids:
count = len(gpu_ids)
@@ -448,26 +455,26 @@ class IntelligenceConfig:
default_model: str = ""
fallback_model: str = ""
model_path: str = "" # Local weights (HF repo, GGUF file, etc.)
checkpoint_path: str = "" # Checkpoint/adapter path
quantization: str = "none" # none, fp8, int8, int4, gguf_q4, gguf_q8
preferred_engine: str = "" # Override engine for this model (e.g., "vllm")
provider: str = "" # local, openai, anthropic, google
model_path: str = "" # Local weights (HF repo, GGUF file, etc.)
checkpoint_path: str = "" # Checkpoint/adapter path
quantization: str = "none" # none, fp8, int8, int4, gguf_q4, gguf_q8
preferred_engine: str = "" # Override engine for this model (e.g., "vllm")
provider: str = "" # local, openai, anthropic, google
# Generation defaults (overridable per-call)
temperature: float = 0.7
max_tokens: int = 1024
top_p: float = 0.9
top_k: int = 40
repetition_penalty: float = 1.0
stop_sequences: str = "" # Comma-separated stop strings
stop_sequences: str = "" # Comma-separated stop strings
@dataclass(slots=True)
class RoutingLearningConfig:
"""Routing sub-policy config within Learning."""
policy: str = "heuristic" # heuristic | learned
min_samples: int = 5 # Min traces before trusting learned routing
policy: str = "heuristic" # heuristic | learned
min_samples: int = 5 # Min traces before trusting learned routing
@dataclass(slots=True)
@@ -716,9 +723,9 @@ class AgentConfig:
default_agent: str = "simple"
max_turns: int = 10
tools: str = "" # comma-separated tool names
objective: str = "" # concise purpose for routing/learning/docs
system_prompt: str = "" # inline system prompt (takes precedence if set)
tools: str = "" # comma-separated tool names
objective: str = "" # concise purpose for routing/learning/docs
system_prompt: str = "" # inline system prompt (takes precedence if set)
system_prompt_path: str = "" # path to system prompt file (.txt, .md)
context_from_memory: bool = True # inject relevant memory context into prompts
@@ -898,7 +905,7 @@ class BlueBubblesChannelConfig:
class WhatsAppBaileysChannelConfig:
"""Per-channel config for WhatsApp via Baileys protocol."""
auth_dir: str = "" # Defaults to ~/.openjarvis/whatsapp_auth
auth_dir: str = "" # Defaults to ~/.openjarvis/whatsapp_auth
assistant_name: str = "Jarvis"
assistant_has_own_number: bool = False
@@ -1176,7 +1183,8 @@ def _migrate_toml_data(data: Dict[str, Any], cfg: "JarvisConfig") -> None:
src = data.get(src_section, {})
if isinstance(src, dict) and "context_injection" in src:
data.setdefault("agent", {}).setdefault(
"context_from_memory", src.pop("context_injection"),
"context_from_memory",
src.pop("context_injection"),
)
if "tools" in data:
@@ -1185,10 +1193,12 @@ def _migrate_toml_data(data: Dict[str, Any], cfg: "JarvisConfig") -> None:
storage_sub = tools_data.get("storage", {})
if isinstance(storage_sub, dict) and "context_injection" in storage_sub:
data.setdefault("agent", {}).setdefault(
"context_from_memory", storage_sub.pop("context_injection"),
"context_from_memory",
storage_sub.pop("context_injection"),
)
@functools.lru_cache(maxsize=1)
def load_config(path: Optional[Path] = None) -> JarvisConfig:
"""Detect hardware, build defaults, overlay TOML overrides.
@@ -1218,16 +1228,31 @@ def load_config(path: Optional[Path] = None) -> JarvisConfig:
# All top-level sections — recursive _apply_toml_section handles
# nested sub-configs (engine.ollama, learning.routing, channel.*, etc.)
top_sections = (
"engine", "intelligence", "learning", "agent",
"server", "telemetry", "traces", "security",
"channel", "tools", "sandbox", "scheduler",
"workflow", "sessions", "a2a", "operators",
"speech", "optimize", "agent_manager",
"engine",
"intelligence",
"learning",
"agent",
"server",
"telemetry",
"traces",
"security",
"channel",
"tools",
"sandbox",
"scheduler",
"workflow",
"sessions",
"a2a",
"operators",
"speech",
"optimize",
"agent_manager",
)
for section_name in top_sections:
if section_name in data:
_apply_toml_section(
getattr(cfg, section_name), data[section_name],
getattr(cfg, section_name),
data[section_name],
)
# Memory: accept [memory] (old) → maps to tools.storage
@@ -1248,13 +1273,8 @@ def generate_minimal_toml(hw: HardwareInfo, engine: str | None = None) -> str:
model = recommend_model(hw, engine)
gpu_comment = ""
if hw.gpu:
mem_label = (
"unified memory" if hw.gpu.vendor == "apple" else "VRAM"
)
gpu_comment = (
f"\n# GPU: {hw.gpu.name}"
f" ({hw.gpu.vram_gb} GB {mem_label})"
)
mem_label = "unified memory" if hw.gpu.vendor == "apple" else "VRAM"
gpu_comment = f"\n# GPU: {hw.gpu.name} ({hw.gpu.vram_gb} GB {mem_label})"
return f"""\
# OpenJarvis configuration
# Hardware: {hw.cpu_brand} ({hw.cpu_count} cores, {hw.ram_gb} GB RAM){gpu_comment}
+171
View File
@@ -0,0 +1,171 @@
"""Tests for agent constructor config-based default resolution."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from openjarvis.agents._stubs import AgentResult, BaseAgent, ToolUsingAgent
class _TestAgent(BaseAgent):
agent_id = "test_cfg"
def run(self, input, context=None, **kwargs):
return AgentResult(content="ok", turns=1)
class _TestToolAgent(ToolUsingAgent):
agent_id = "test_cfg_tool"
def run(self, input, context=None, **kwargs):
return AgentResult(content="ok", turns=1)
class _TestToolAgentWithDefaults(ToolUsingAgent):
"""Agent with class-level defaults (like MonitorOperativeAgent)."""
agent_id = "test_cfg_tool_defaults"
_default_temperature = 0.3
_default_max_tokens = 4096
_default_max_turns = 25
def run(self, input, context=None, **kwargs):
return AgentResult(content="ok", turns=1)
class TestBaseAgentConfigResolution:
"""BaseAgent resolves None params from config.intelligence."""
def test_none_temperature_reads_config(self):
"""When temperature is not passed, it should come from config."""
engine = MagicMock()
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
mock_cfg.return_value.intelligence.temperature = 0.2
mock_cfg.return_value.intelligence.max_tokens = 512
agent = _TestAgent(engine, "m")
assert agent._temperature == 0.2
def test_none_max_tokens_reads_config(self):
"""When max_tokens is not passed, it should come from config."""
engine = MagicMock()
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
mock_cfg.return_value.intelligence.temperature = 0.7
mock_cfg.return_value.intelligence.max_tokens = 512
agent = _TestAgent(engine, "m")
assert agent._max_tokens == 512
def test_explicit_temperature_overrides_config(self):
"""Caller-provided temperature takes precedence over config."""
engine = MagicMock()
agent = _TestAgent(engine, "m", temperature=0.9)
assert agent._temperature == 0.9
def test_explicit_max_tokens_overrides_config(self):
"""Caller-provided max_tokens takes precedence over config."""
engine = MagicMock()
agent = _TestAgent(engine, "m", max_tokens=2048)
assert agent._max_tokens == 2048
def test_partial_override_temperature_only(self):
"""Providing only temperature still reads max_tokens from config."""
engine = MagicMock()
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
mock_cfg.return_value.intelligence.temperature = 0.2
mock_cfg.return_value.intelligence.max_tokens = 512
agent = _TestAgent(engine, "m", temperature=0.9)
assert agent._temperature == 0.9
assert agent._max_tokens == 512
def test_config_load_failure_uses_hardcoded_fallback(self):
"""When config loading fails, fall back to class defaults then 0.7/1024."""
engine = MagicMock()
with patch(
"openjarvis.agents._stubs.load_config",
side_effect=Exception("boom"),
):
agent = _TestAgent(engine, "m")
assert agent._temperature == 0.7
assert agent._max_tokens == 1024
class TestToolUsingAgentConfigResolution:
"""ToolUsingAgent resolves None max_turns from config.agent."""
def test_none_max_turns_reads_config(self):
"""When max_turns is not passed, it should come from config."""
engine = MagicMock()
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
mock_cfg.return_value.intelligence.temperature = 0.7
mock_cfg.return_value.intelligence.max_tokens = 1024
mock_cfg.return_value.agent.max_turns = 15
agent = _TestToolAgent(engine, "m")
assert agent._max_turns == 15
def test_explicit_max_turns_overrides_config(self):
"""Caller-provided max_turns takes precedence over config."""
engine = MagicMock()
agent = _TestToolAgent(engine, "m", max_turns=5)
assert agent._max_turns == 5
def test_temperature_and_max_tokens_forwarded_to_base(self):
"""ToolUsingAgent also resolves temperature/max_tokens from config."""
engine = MagicMock()
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
mock_cfg.return_value.intelligence.temperature = 0.1
mock_cfg.return_value.intelligence.max_tokens = 256
mock_cfg.return_value.agent.max_turns = 10
agent = _TestToolAgent(engine, "m")
assert agent._temperature == 0.1
assert agent._max_tokens == 256
def test_config_load_failure_max_turns_fallback(self):
"""When config loading fails, max_turns falls back to class default then 10."""
engine = MagicMock()
with patch(
"openjarvis.agents._stubs.load_config",
side_effect=Exception("boom"),
):
agent = _TestToolAgent(engine, "m")
assert agent._max_turns == 10
class TestClassLevelDefaults:
"""Agents with class-level _default_* attributes use them as fallback."""
def test_class_default_used_when_config_fails(self):
"""Agent class defaults are used when config is unavailable."""
engine = MagicMock()
with patch(
"openjarvis.agents._stubs.load_config",
side_effect=Exception("boom"),
):
agent = _TestToolAgentWithDefaults(engine, "m")
assert agent._temperature == 0.3
assert agent._max_tokens == 4096
assert agent._max_turns == 25
def test_config_overrides_class_default(self):
"""User config takes precedence over class-level defaults."""
engine = MagicMock()
with patch("openjarvis.agents._stubs.load_config") as mock_cfg:
mock_cfg.return_value.intelligence.temperature = 0.5
mock_cfg.return_value.intelligence.max_tokens = 2048
mock_cfg.return_value.agent.max_turns = 12
agent = _TestToolAgentWithDefaults(engine, "m")
assert agent._temperature == 0.5
assert agent._max_tokens == 2048
assert agent._max_turns == 12
def test_explicit_overrides_everything(self):
"""Caller-provided values override both config and class defaults."""
engine = MagicMock()
agent = _TestToolAgentWithDefaults(
engine,
"m",
temperature=0.9,
max_tokens=100,
max_turns=2,
)
assert agent._temperature == 0.9
assert agent._max_tokens == 100
assert agent._max_turns == 2