diff --git a/src/openjarvis/agents/_stubs.py b/src/openjarvis/agents/_stubs.py
index 9a91c90b..66b8755e 100644
--- a/src/openjarvis/agents/_stubs.py
+++ b/src/openjarvis/agents/_stubs.py
@@ -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 ... blocks
text = re.sub(
- r".*?\s*", "", text,
+ r".*?\s*",
+ "",
+ text,
flags=re.DOTALL | re.IGNORECASE,
)
# Leading content before a bare (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
diff --git a/src/openjarvis/agents/claude_code.py b/src/openjarvis/agents/claude_code.py
index 59aaefc1..8fb77473 100644
--- a/src/openjarvis/agents/claude_code.py
+++ b/src/openjarvis/agents/claude_code.py
@@ -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)
diff --git a/src/openjarvis/agents/monitor_operative.py b/src/openjarvis/agents/monitor_operative.py
index a764ca35..3965e3d4 100644
--- a/src/openjarvis/agents/monitor_operative.py
+++ b/src/openjarvis/agents/monitor_operative.py
@@ -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(
diff --git a/src/openjarvis/agents/native_openhands.py b/src/openjarvis/agents/native_openhands.py
index 46570d53..cf5579cd 100644
--- a/src/openjarvis/agents/native_openhands.py
+++ b/src/openjarvis/agents/native_openhands.py
@@ -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 ... or blocks
text = re.sub(r".*?\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,
)
diff --git a/src/openjarvis/agents/native_react.py b/src/openjarvis/agents/native_react.py
index cfcc5023..75cd8efc 100644
--- a/src/openjarvis/agents/native_react.py
+++ b/src/openjarvis/agents/native_react.py
@@ -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(
diff --git a/src/openjarvis/agents/openhands.py b/src/openjarvis/agents/openhands.py
index 87dad601..4432b655 100644
--- a/src/openjarvis/agents/openhands.py
+++ b/src/openjarvis/agents/openhands.py
@@ -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", "")
diff --git a/src/openjarvis/agents/operative.py b/src/openjarvis/agents/operative.py
index 7f88f840..02c1d725 100644
--- a/src/openjarvis/agents/operative.py
+++ b/src/openjarvis/agents/operative.py
@@ -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)
diff --git a/src/openjarvis/agents/orchestrator.py b/src/openjarvis/agents/orchestrator.py
index ffbff90e..cca03d2c 100644
--- a/src/openjarvis/agents/orchestrator.py
+++ b/src/openjarvis/agents/orchestrator.py
@@ -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 ""
diff --git a/src/openjarvis/agents/rlm.py b/src/openjarvis/agents/rlm.py
index 0cc6e3a4..cd8c0c37 100644
--- a/src/openjarvis/agents/rlm.py
+++ b/src/openjarvis/agents/rlm.py
@@ -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,
diff --git a/src/openjarvis/core/config.py b/src/openjarvis/core/config.py
index 5ca8fa77..6c6672f2 100644
--- a/src/openjarvis/core/config.py
+++ b/src/openjarvis/core/config.py
@@ -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}
diff --git a/tests/agents/test_config_defaults.py b/tests/agents/test_config_defaults.py
new file mode 100644
index 00000000..d3f2b117
--- /dev/null
+++ b/tests/agents/test_config_defaults.py
@@ -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