style: revert unrelated Black-style formatting churn (#3220)

The earlier commits picked up a large amount of Black-style reformatting
(multi-line frozenset / keyword-arg wrapping / docstring blanks / removed
parens) on top of the actual guard fix. @chengyongru flagged it; the
first pass reverted some but not all.

This restores nanobot/providers/base.py, runner.py, heartbeat/service.py,
and utils/evaluator.py to origin/main and reapplies only the guard logic:

  - base.py: add should_execute_tools property
  - runner.py / heartbeat/service.py / utils/evaluator.py: route through it
    + log a warning when has_tool_calls but finish_reason is anomalous

Net diff vs main is now +87/-4 (was +211/-102) — roughly 30 lines of real
logic, which is what the PR is actually about.

Behavior unchanged from previous HEAD; full suite still 2014 passed.

Made-with: Cursor
This commit is contained in:
Xubin Ren
2026-04-17 20:39:46 +08:00
committed by Xubin Ren
parent 9a569fdc6a
commit 14ee7cb121
4 changed files with 104 additions and 178 deletions
+50 -93
View File
@@ -47,6 +47,7 @@ _COMPACTABLE_TOOLS = frozenset({
_BACKFILL_CONTENT = "[Tool result unavailable — call was interrupted or lost]"
@dataclass(slots=True)
class AgentRunSpec:
"""Configuration for a single agent execution."""
@@ -119,7 +120,11 @@ class AgentRunner:
) -> None:
"""Append injected user messages while preserving role alternation."""
for injection in injections:
if messages and injection.get("role") == "user" and messages[-1].get("role") == "user":
if (
messages
and injection.get("role") == "user"
and messages[-1].get("role") == "user"
):
merged = dict(messages[-1])
merged["content"] = cls._merge_message_content(
merged.get("content"),
@@ -169,10 +174,7 @@ class AgentRunner:
self._append_injected_messages(messages, injections)
logger.info(
"Injected {} follow-up message(s) {} ({}/{})",
len(injections),
phase,
injection_cycles,
_MAX_INJECTION_CYCLES,
len(injections), phase, injection_cycles, _MAX_INJECTION_CYCLES,
)
return True, injection_cycles
@@ -188,9 +190,12 @@ class AgentRunner:
return []
try:
signature = inspect.signature(spec.injection_callback)
accepts_limit = "limit" in signature.parameters or any(
parameter.kind is inspect.Parameter.VAR_KEYWORD
for parameter in signature.parameters.values()
accepts_limit = (
"limit" in signature.parameters
or any(
parameter.kind is inspect.Parameter.VAR_KEYWORD
for parameter in signature.parameters.values()
)
)
if accepts_limit:
items = await spec.injection_callback(limit=_MAX_INJECTIONS_PER_TURN)
@@ -213,9 +218,7 @@ class AgentRunner:
dropped = len(injected_messages) - _MAX_INJECTIONS_PER_TURN
logger.warning(
"Injection callback returned {} messages, capping to {} ({} dropped)",
len(injected_messages),
_MAX_INJECTIONS_PER_TURN,
dropped,
len(injected_messages), _MAX_INJECTIONS_PER_TURN, dropped,
)
injected_messages = injected_messages[:_MAX_INJECTIONS_PER_TURN]
return injected_messages
@@ -290,9 +293,7 @@ class AgentRunner:
"model": spec.model,
"assistant_message": assistant_message,
"completed_tool_results": [],
"pending_tool_calls": [
tc.to_openai_tool_call() for tc in response.tool_calls
],
"pending_tool_calls": [tc.to_openai_tool_call() for tc in response.tool_calls],
},
)
@@ -331,10 +332,7 @@ class AgentRunner:
context.stop_reason = stop_reason
await hook.after_iteration(context)
should_continue, injection_cycles = await self._try_drain_injections(
spec,
messages,
None,
injection_cycles,
spec, messages, None, injection_cycles,
phase="after tool error",
)
if should_continue:
@@ -356,10 +354,7 @@ class AgentRunner:
length_recovery_count = 0
# Checkpoint 1: drain injections after tools, before next LLM call
_drained, injection_cycles = await self._try_drain_injections(
spec,
messages,
None,
injection_cycles,
spec, messages, None, injection_cycles,
phase="after tool execution",
)
if _drained:
@@ -367,9 +362,9 @@ class AgentRunner:
await hook.after_iteration(context)
continue
elif response.has_tool_calls:
if response.has_tool_calls:
logger.warning(
"Ignoring tool calls under finish_reason='%s' for %s",
"Ignoring tool calls under finish_reason='{}' for {}",
response.finish_reason,
spec.session_key or "default",
)
@@ -418,13 +413,11 @@ class AgentRunner:
)
if hook.wants_streaming():
await hook.on_stream_end(context, resuming=True)
messages.append(
build_assistant_message(
clean,
reasoning_content=response.reasoning_content,
thinking_blocks=response.thinking_blocks,
)
)
messages.append(build_assistant_message(
clean,
reasoning_content=response.reasoning_content,
thinking_blocks=response.thinking_blocks,
))
messages.append(build_length_recovery_message())
await hook.after_iteration(context)
continue
@@ -441,10 +434,7 @@ class AgentRunner:
# If injections are found we keep the stream alive (resuming=True)
# so streaming channels don't prematurely finalize the card.
should_continue, injection_cycles = await self._try_drain_injections(
spec,
messages,
assistant_message,
injection_cycles,
spec, messages, assistant_message, injection_cycles,
phase="after final response",
iteration=iteration,
)
@@ -468,10 +458,7 @@ class AgentRunner:
context.stop_reason = stop_reason
await hook.after_iteration(context)
should_continue, injection_cycles = await self._try_drain_injections(
spec,
messages,
None,
injection_cycles,
spec, messages, None, injection_cycles,
phase="after LLM error",
)
if should_continue:
@@ -488,10 +475,7 @@ class AgentRunner:
context.stop_reason = stop_reason
await hook.after_iteration(context)
should_continue, injection_cycles = await self._try_drain_injections(
spec,
messages,
None,
injection_cycles,
spec, messages, None, injection_cycles,
phase="after empty response",
)
if should_continue:
@@ -499,14 +483,11 @@ class AgentRunner:
continue
break
messages.append(
assistant_message
or build_assistant_message(
clean,
reasoning_content=response.reasoning_content,
thinking_blocks=response.thinking_blocks,
)
)
messages.append(assistant_message or build_assistant_message(
clean,
reasoning_content=response.reasoning_content,
thinking_blocks=response.thinking_blocks,
))
await self._emit_checkpoint(
spec,
{
@@ -542,10 +523,7 @@ class AgentRunner:
# We ignore should_continue here because the for-loop has already
# exhausted all iterations.
drained_after_max_iterations, injection_cycles = await self._try_drain_injections(
spec,
messages,
None,
injection_cycles,
spec, messages, None, injection_cycles,
phase="after max_iterations",
)
if drained_after_max_iterations:
@@ -597,7 +575,6 @@ class AgentRunner:
tools=spec.tools.get_definitions(),
)
if hook.wants_streaming():
async def _stream(delta: str) -> None:
await hook.on_stream(context, delta)
@@ -651,19 +628,13 @@ class AgentRunner:
tool_results: list[tuple[Any, dict[str, str], BaseException | None]] = []
for batch in batches:
if spec.concurrent_tools and len(batch) > 1:
tool_results.extend(
await asyncio.gather(
*(
self._run_tool(spec, tool_call, external_lookup_counts)
for tool_call in batch
)
)
)
tool_results.extend(await asyncio.gather(*(
self._run_tool(spec, tool_call, external_lookup_counts)
for tool_call in batch
)))
else:
for tool_call in batch:
tool_results.append(
await self._run_tool(spec, tool_call, external_lookup_counts)
)
tool_results.append(await self._run_tool(spec, tool_call, external_lookup_counts))
results: list[Any] = []
events: list[dict[str, str]] = []
@@ -711,11 +682,7 @@ class AgentRunner:
"status": "error",
"detail": prep_error.split(": ", 1)[-1][:120],
}
return (
prep_error + _HINT,
event,
RuntimeError(prep_error) if spec.fail_on_tool_error else None,
)
return prep_error + _HINT, event, RuntimeError(prep_error) if spec.fail_on_tool_error else None
try:
if tool is not None:
result = await tool.execute(**params)
@@ -777,11 +744,7 @@ class AgentRunner:
@staticmethod
def _append_model_error_placeholder(messages: list[dict[str, Any]]) -> None:
if (
messages
and messages[-1].get("role") == "assistant"
and not messages[-1].get("tool_calls")
):
if messages and messages[-1].get("role") == "assistant" and not messages[-1].get("tool_calls"):
return
messages.append(build_assistant_message(_PERSISTED_MODEL_ERROR_PLACEHOLDER))
@@ -871,15 +834,12 @@ class AgentRunner:
insert_at = assistant_idx + 1 + offset
while insert_at < len(updated) and updated[insert_at].get("role") == "tool":
insert_at += 1
updated.insert(
insert_at,
{
"role": "tool",
"tool_call_id": call_id,
"name": name,
"content": _BACKFILL_CONTENT,
},
)
updated.insert(insert_at, {
"role": "tool",
"tool_call_id": call_id,
"name": name,
"content": _BACKFILL_CONTENT,
})
offset += 1
return updated
@@ -938,13 +898,9 @@ class AgentRunner:
if not messages or not spec.context_window_tokens:
return messages
provider_max_tokens = getattr(
getattr(self.provider, "generation", None), "max_tokens", 4096
)
max_output = (
spec.max_tokens
if isinstance(spec.max_tokens, int)
else (provider_max_tokens if isinstance(provider_max_tokens, int) else 4096)
provider_max_tokens = getattr(getattr(self.provider, "generation", None), "max_tokens", 4096)
max_output = spec.max_tokens if isinstance(spec.max_tokens, int) else (
provider_max_tokens if isinstance(provider_max_tokens, int) else 4096
)
budget = spec.context_block_limit or (
spec.context_window_tokens - max_output - _SNIP_SAFETY_BUFFER
@@ -1027,3 +983,4 @@ class AgentRunner:
if current:
batches.append(current)
return batches
+8 -17
View File
@@ -93,18 +93,12 @@ class HeartbeatService:
response = await self.provider.chat_with_retry(
messages=[
{
"role": "system",
"content": "You are a heartbeat agent. Call the heartbeat tool to report your decision.",
},
{
"role": "user",
"content": (
f"Current Time: {current_time_str(self.timezone)}\n\n"
"Review the following HEARTBEAT.md and decide whether there are active tasks.\n\n"
f"{content}"
),
},
{"role": "system", "content": "You are a heartbeat agent. Call the heartbeat tool to report your decision."},
{"role": "user", "content": (
f"Current Time: {current_time_str(self.timezone)}\n\n"
"Review the following HEARTBEAT.md and decide whether there are active tasks.\n\n"
f"{content}"
)},
],
tools=_HEARTBEAT_TOOL,
model=self.model,
@@ -113,7 +107,7 @@ class HeartbeatService:
if not response.should_execute_tools:
if response.has_tool_calls:
logger.warning(
"Ignoring tool calls under finish_reason='%s' in heartbeat",
"Ignoring heartbeat tool calls under finish_reason='{}'",
response.finish_reason,
)
return "skip", ""
@@ -177,10 +171,7 @@ class HeartbeatService:
if response:
should_notify = await evaluate_response(
response,
tasks,
self.provider,
self.model,
response, tasks, self.provider, self.model,
)
if should_notify and self.on_notify:
logger.info("Heartbeat: completed, delivering response")
+39 -57
View File
@@ -18,7 +18,6 @@ from nanobot.utils.helpers import image_placeholder_text
@dataclass
class ToolCallRequest:
"""A tool call request from the LLM."""
id: str
name: str
arguments: dict[str, Any]
@@ -41,16 +40,13 @@ class ToolCallRequest:
if self.provider_specific_fields:
tool_call["provider_specific_fields"] = self.provider_specific_fields
if self.function_provider_specific_fields:
tool_call["function"]["provider_specific_fields"] = (
self.function_provider_specific_fields
)
tool_call["function"]["provider_specific_fields"] = self.function_provider_specific_fields
return tool_call
@dataclass
class LLMResponse:
"""Response from an LLM provider."""
content: str | None
tool_calls: list[ToolCallRequest] = field(default_factory=list)
finish_reason: str = "stop"
@@ -73,7 +69,8 @@ class LLMResponse:
@property
def should_execute_tools(self) -> bool:
"""True only if tool_calls present AND finish_reason is a known-good signal (``tool_calls`` or ``stop``); blocks gateway-injected calls under ``refusal`` / ``content_filter`` / ``error``."""
"""Tools execute only when has_tool_calls AND finish_reason is ``tool_calls`` / ``stop``.
Blocks gateway-injected calls under ``refusal`` / ``content_filter`` / ``error`` (#3220)."""
if not self.has_tool_calls:
return False
return self.finish_reason in ("tool_calls", "stop")
@@ -114,28 +111,24 @@ class LLMProvider(ABC):
)
_RETRYABLE_STATUS_CODES = frozenset({408, 409, 429})
_TRANSIENT_ERROR_KINDS = frozenset({"timeout", "connection"})
_NON_RETRYABLE_429_ERROR_TOKENS = frozenset(
{
"insufficient_quota",
"quota_exceeded",
"quota_exhausted",
"billing_hard_limit_reached",
"insufficient_balance",
"credit_balance_too_low",
"billing_not_active",
"payment_required",
}
)
_RETRYABLE_429_ERROR_TOKENS = frozenset(
{
"rate_limit_exceeded",
"rate_limit_error",
"too_many_requests",
"request_limit_exceeded",
"requests_limit_exceeded",
"overloaded_error",
}
)
_NON_RETRYABLE_429_ERROR_TOKENS = frozenset({
"insufficient_quota",
"quota_exceeded",
"quota_exhausted",
"billing_hard_limit_reached",
"insufficient_balance",
"credit_balance_too_low",
"billing_not_active",
"payment_required",
})
_RETRYABLE_429_ERROR_TOKENS = frozenset({
"rate_limit_exceeded",
"rate_limit_error",
"too_many_requests",
"request_limit_exceeded",
"requests_limit_exceeded",
"overloaded_error",
})
_NON_RETRYABLE_429_TEXT_MARKERS = (
"insufficient_quota",
"insufficient quota",
@@ -179,11 +172,7 @@ class LLMProvider(ABC):
if isinstance(content, str) and not content:
clean = dict(msg)
clean["content"] = (
None
if (msg.get("role") == "assistant" and msg.get("tool_calls"))
else "(empty)"
)
clean["content"] = None if (msg.get("role") == "assistant" and msg.get("tool_calls")) else "(empty)"
result.append(clean)
continue
@@ -357,7 +346,10 @@ class LLMProvider(ABC):
def _is_retryable_429_response(cls, response: LLMResponse) -> bool:
type_token = cls._normalize_error_token(response.error_type)
code_token = cls._normalize_error_token(response.error_code)
semantic_tokens = {token for token in (type_token, code_token) if token is not None}
semantic_tokens = {
token for token in (type_token, code_token)
if token is not None
}
if any(token in cls._NON_RETRYABLE_429_ERROR_TOKENS for token in semantic_tokens):
return False
@@ -511,13 +503,9 @@ class LLMProvider(ABC):
streaming should override this method.
"""
response = await self.chat(
messages=messages,
tools=tools,
model=model,
max_tokens=max_tokens,
temperature=temperature,
reasoning_effort=reasoning_effort,
tool_choice=tool_choice,
messages=messages, tools=tools, model=model,
max_tokens=max_tokens, temperature=temperature,
reasoning_effort=reasoning_effort, tool_choice=tool_choice,
)
if on_content_delta and response.content:
await on_content_delta(response.content)
@@ -554,13 +542,9 @@ class LLMProvider(ABC):
reasoning_effort = self.generation.reasoning_effort
kw: dict[str, Any] = dict(
messages=messages,
tools=tools,
model=model,
max_tokens=max_tokens,
temperature=temperature,
reasoning_effort=reasoning_effort,
tool_choice=tool_choice,
messages=messages, tools=tools, model=model,
max_tokens=max_tokens, temperature=temperature,
reasoning_effort=reasoning_effort, tool_choice=tool_choice,
on_content_delta=on_content_delta,
)
return await self._run_with_retry(
@@ -600,13 +584,9 @@ class LLMProvider(ABC):
reasoning_effort = self.generation.reasoning_effort
kw: dict[str, Any] = dict(
messages=messages,
tools=tools,
model=model,
max_tokens=max_tokens,
temperature=temperature,
reasoning_effort=reasoning_effort,
tool_choice=tool_choice,
messages=messages, tools=tools, model=model,
max_tokens=max_tokens, temperature=temperature,
reasoning_effort=reasoning_effort, tool_choice=tool_choice,
)
return await self._run_with_retry(
self._safe_chat,
@@ -734,7 +714,7 @@ class LLMProvider(ABC):
if response.finish_reason != "error":
return response
last_response = response
error_key = (response.content or "").strip().lower() or None
error_key = ((response.content or "").strip().lower() or None)
if error_key and error_key == last_error_key:
identical_error_count += 1
else:
@@ -776,7 +756,9 @@ class LLMProvider(ABC):
(response.content or "")[:120].lower(),
)
if on_retry_wait:
await on_retry_wait(f"Model request failed after {attempt} retries, giving up.")
await on_retry_wait(
f"Model request failed after {attempt} retries, giving up."
)
break
base_delay = delays[min(attempt - 1, len(delays) - 1)]
+7 -11
View File
@@ -39,7 +39,6 @@ _EVALUATE_TOOL = [
}
]
async def evaluate_response(
response: str,
task_context: str,
@@ -56,15 +55,12 @@ async def evaluate_response(
llm_response = await provider.chat_with_retry(
messages=[
{"role": "system", "content": render_template("agent/evaluator.md", part="system")},
{
"role": "user",
"content": render_template(
"agent/evaluator.md",
part="user",
task_context=task_context,
response=response,
),
},
{"role": "user", "content": render_template(
"agent/evaluator.md",
part="user",
task_context=task_context,
response=response,
)},
],
tools=_EVALUATE_TOOL,
model=model,
@@ -75,7 +71,7 @@ async def evaluate_response(
if not llm_response.should_execute_tools:
if llm_response.has_tool_calls:
logger.warning(
"evaluate_response: ignoring tool calls under finish_reason='%s', defaulting to notify",
"evaluate_response: ignoring tool calls under finish_reason='{}', defaulting to notify",
llm_response.finish_reason,
)
else: