Skip to content
8 changes: 1 addition & 7 deletions packages/nooa-bench/src/nooa_bench/bench_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,7 @@ async def _run_evaluation(self, task_input: dict) -> dict:
_logger.error("BenchAgent failed: %s", e)
return {"response": "", "success": False, "error": str(e)}

@strategy(
CodeActStrategy(
config=CodeActConfig(
max_iterations=300, max_retries=10, text_only_stop_behavior="synthetic_comment"
)
)
)
@strategy(CodeActStrategy(config=CodeActConfig(max_iterations=300, max_retries=10)))
async def _solve_task(self, description: str) -> TaskResult:
"""Solve the task.

Expand Down
3 changes: 1 addition & 2 deletions skills/nooa-codeact-advanced/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,14 @@ class Notifier(Agent, llm=llm):
| `max_iterations` | `None` | **Unlimited.** The loop then stops only on completion, the error budget, or a hard abort. |
| `max_retries` | `3` | **Cumulative session error budget, not consecutive** (the counter is never reset). LLM API errors, bad tool JSON, empty code, and `return_result` validation failures all count. |
| `max_consecutive_text_only` | `3` | Consecutive no-tool-call text replies before hard abort; `0` disables. Any real tool call resets the counter. |
| `text_only_stop_behavior` | `"return_result"` | Text-only reply → try to validate the text as the final result; on failure, a visible correction `Error` is added. `"synthetic_reasoning"` instead converts the text to a no-op `reasoning(...)` cell whose tool result says the task is NOT finished. |
| `cell_timeout` | `None` | Per-cell `asyncio.wait_for` limit in seconds; `None` = unlimited. Cannot interrupt a truly blocking sync syscall — that's what the blocking-call AST validation is for. |
| `max_tokens` / `temperature` / `top_p` | `None` | Passed to every generation call when set (model defaults otherwise). On empty responses with `finish_reason="length"` CodeAct aborts and tells you to raise `max_tokens` (16384+ for reasoning models). |
| `translate_tool_calls` | `False` | When a weak model calls an agent method directly as a tool (instead of via `execute_python`), rewrite it into equivalent code and run it — teaching the right pattern. Off = error listing the two valid tools. |
| `restrictions` | `RestrictionsConfig()` | See Restrictions below. |
| `prefill` | `InspectInputsPrefill()` | See Prefill above. |
| `max_tool_calls` | `None` | **Dead — declared but never read.** Setting it does nothing. |

`tool_choice` is hardcoded `"auto"`. There is no `allow_text_response` option (older docs mention one) — text handling is entirely the two text-only knobs.
`tool_choice` is hardcoded `"auto"`. Text-only output is retained as the original assistant turn. By default, CodeAct appends a visible `Error` asking the model to use a tool and retries. To accept bare text as the result instead, construct the strategy with `CodeActStrategy(on_text_only=return_text_as_result)`. Custom callbacks may return a `TextOnlyResponseAction` to append feedback or synthesize an `execute_python` call without replacing the original turn.

## `return_result` mechanics

Expand Down
10 changes: 10 additions & 0 deletions src/nooa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@
GenerationStrategy,
InspectInputsPrefill,
PredictStrategy,
TextOnlyResponseAction,
TextOnlyResponseContext,
TextOnlyResponseHandler,
get_default_strategy,
retry_text_only_response,
return_text_as_result,
set_default_strategy,
)
from nooa.strategy_validation import ( # noqa: E402
Expand Down Expand Up @@ -119,6 +124,11 @@ def __getattr__(name):
# Strategies
"GenerationStrategy",
"CodeActStrategy",
"TextOnlyResponseAction",
"TextOnlyResponseContext",
"TextOnlyResponseHandler",
"retry_text_only_response",
"return_text_as_result",
"CodeActLiteStrategy",
"ReflexionStrategy",
"PredictStrategy",
Expand Down
20 changes: 1 addition & 19 deletions src/nooa/config/strategy_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,8 @@ class CodeActConfig(BaseModel):
max_retries: int = 3
# Maximum consecutive turns where the LLM returns plain text instead of a
# tool call before the run is aborted. A real tool call resets the counter.
# Set to 0 to disable the guard (legacy behavior). See also
# text_only_stop_behavior for how each text-only response is handled.
# Set to 0 to disable the guard.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
max_consecutive_text_only: int = 3
# How to handle finish_reason="stop" (text-only, no tool call) responses:
# - "return_result": Route through return_result(content) validation. If the
# return type matches, the session terminates cleanly. If not, the LLM gets
# an actionable validation error to self-correct. (Recommended — breaks
# loops faster and often terminates successfully.)
# - "synthetic_comment": Convert to an execute_python call whose code is the
# text as a `#` comment — a no-op synthetic call that preserves the text
# in traces. The LLM sees "status: complete" and must still call
# return_result() explicitly.
text_only_stop_behavior: Literal["return_result", "synthetic_comment"] = "return_result"

@field_validator("text_only_stop_behavior", mode="before")
@classmethod
def _migrate_synthetic_reasoning(cls, v: str) -> str:
if v == "synthetic_reasoning":
return "synthetic_comment"
return v

cell_timeout: float | None = None
max_tokens: int | None = None
Expand Down
19 changes: 7 additions & 12 deletions src/nooa/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class TextOnlyReply(EventBase): # type: ignore[misc]
but ``Role.METADATA`` — never rendered to the model, so capturing a
text-only reply does not change generation. This is the durable, structured record that
powers ``/bug`` capture and time-travel replay; the model-visible
correction is a separate ``Error``/``Feedback`` event (``Role.USER``).
recovery action is recorded separately and may be supplied by the host.

Replaces the lossy ``DebugTrace`` previously written on the CodeAct
text-only path, which truncated the content and could not be relied on by
Expand All @@ -141,22 +141,17 @@ class TextOnlyReply(EventBase): # type: ignore[misc]
finish_reason: Annotated[
str, Field(description="LLM finish_reason for the text-only turn (e.g. 'stop')")
] = ""
route: Annotated[
handler: Annotated[
str,
Field(description="Handling route: 'return_result' or 'synthetic_comment'"),
Field(description="Qualified name of the text-only response handler"),
] = ""
action: Annotated[
str,
Field(description="Handler action: 'return_result', 'retry', or 'tool_calls'"),
] = ""
consecutive_text_only: Annotated[
int, Field(description="Count of consecutive text-only turns including this one")
] = 0
recovered: Annotated[
bool,
Field(
description=(
"Set True on a later turn if the model issued a real tool call "
"after this text-only reply (i.e. the corrective feedback worked)."
)
),
] = False


class LLMOutput(EventBase): # type: ignore[misc]
Expand Down
14 changes: 13 additions & 1 deletion src/nooa/strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@

from nooa.config import CodeActConfig
from nooa.strategies.base import GenerationStrategy, RuntimeServices
from nooa.strategies.codeact import CodeActStrategy
from nooa.strategies.codeact import (
CodeActStrategy,
TextOnlyResponseAction,
TextOnlyResponseContext,
TextOnlyResponseHandler,
retry_text_only_response,
return_text_as_result,
)
from nooa.strategies.codeact_lite import CodeActLiteStrategy
from nooa.strategies.composite import CompositeStrategy
from nooa.strategies.current_call import CurrentCall
Expand Down Expand Up @@ -91,6 +98,11 @@ def set_default_strategy(strategy: GenerationStrategy | None) -> None:
"CompositeStrategy",
"TemplateStrategy",
"CodeActStrategy",
"TextOnlyResponseAction",
"TextOnlyResponseContext",
"TextOnlyResponseHandler",
"retry_text_only_response",
"return_text_as_result",
"CodeActLiteStrategy",
"ReflexionStrategy",
"PredictStrategy",
Expand Down
Loading
Loading