Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/jumar/decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

# Retriable: retry once before failing as unverifiable_plan.
_RETRIABLE: frozenset[str] = frozenset(
{"missing_check", "parse_error", "timed_out", "empty_response"}
{"missing_check", "parse_error", "timed_out", "empty_response", "harness_error"}
)

# Non-retriable: fail immediately with the mapped FailureCode.
Expand Down Expand Up @@ -462,6 +462,7 @@ def decompose(
api_key_env=resolved.api_key_env,
reasoning_effort=resolved.reasoning_effort,
max_tokens=resolved.max_tokens,
max_tool_steps=resolved.max_tool_steps,
commands_allow=config.commands.allow,
commands_deny=config.commands.deny,
)
Expand Down Expand Up @@ -524,7 +525,32 @@ def decompose(
rejection: str | None
detail: str | None
subtasks: tuple[Subtask, ...]
if result.timed_out:
# The endpoint was never reached, or refused the call. There is no
# response to parse, so reporting this as "response is not valid JSON"
# is a lie: a stopped LM Studio produced `parse_error` with
# prompt_tokens 0 and completion_tokens 0 and read exactly like the
# model failures it was being compared against, twice, on 5 and 6 Sep.
# `detect_harness_error` and `FailureCode.harness_error` already
# existed; nothing consumed them.
#
# Guarded on exit_status because `detect_harness_error` scans stdout
# as well as stderr, so a plan whose own text contains "connection
# refused" would otherwise be discarded as an outage. A call that
# returned a response has exit_status 0.
#
# Message only. The retry path and the FailureCode are deliberately
# unchanged (see test_harness_outage_journalled_as_harness_error and
# test_regression_run_20260812_0525_c9f7_...): an outage is still
# retried once and still fails as unverifiable_plan. All that changes
# is that the rejection now says what actually happened instead of
# "response is not valid JSON".
if harness_error is not None and result.exit_status != 0:
subtasks, rejection, detail = (
(),
"harness_error",
f"{harness_error}: {(result.stderr or result.stdout).strip()[:200]}".strip(),
)
elif result.timed_out:
subtasks, rejection, detail = (
(),
"timed_out",
Expand Down
1 change: 1 addition & 0 deletions src/jumar/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def execute(
api_key_env=resolved.api_key_env,
reasoning_effort=resolved.reasoning_effort,
max_tokens=resolved.max_tokens,
max_tool_steps=resolved.max_tool_steps,
commands_allow=config.commands.allow,
commands_deny=config.commands.deny,
)
Expand Down
6 changes: 6 additions & 0 deletions src/jumar/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ class HarnessInfo:
api_key_env: str | None = None
reasoning_effort: str | None = None
max_tokens: int | None = None
# Read by openai_agent.run_openai_agent as the tool-calling step cap.
# Without this field the getattr() there always fell through to the
# hard-coded MAX_TOOL_STEPS of 20, so `max_tool_steps` in jumar.toml was
# silently inert: a run configured for 35 still reported
# "tool-call step cap (20) exceeded".
max_tool_steps: int | None = None
commands_allow: tuple[str, ...] = ()
commands_deny: tuple[str, ...] = ()

Expand Down
Loading