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
46 changes: 43 additions & 3 deletions src/jumar/openai_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ def _tool_run_command(
cwd=cwd,
capture_output=True,
timeout=120,
# No stdin. A check whose argv omits its file operand -- `grep -qE
# PATTERN` with no path -- otherwise inherits jumar's stdin and
# blocks on it until the timeout. Seen 6 Sep: 3 x 300s on one
# subtask, then repairs_exhausted, for a command that exits
# immediately against a closed stdin.
stdin=subprocess.DEVNULL,
shell=False,
check=False,
)
Expand Down Expand Up @@ -460,6 +466,33 @@ def _rate() -> dict[str, Any]:
message = choices[0].get("message") or {}
content = str(message.get("content") or "")
tool_calls = message.get("tool_calls") or []

# Fall back to `reasoning_content` when there is nothing else.
#
# The chat-completions schema has no reasoning field, so servers that
# separate thinking from the answer invent one; LM Studio calls it
# `reasoning_content`. Reading only `content` throws that away, and a
# model that answers into it returns exit_status 0 carrying nothing --
# a successful call that said nothing, which reads downstream as an
# empty result and passes checks an earlier attempt already satisfied.
# Reproduced 6 Sep on a 16-token request: `content` empty, the whole
# answer in `reasoning_content`.
#
# Deliberately narrow. This is reasoning, not a composed answer, so it
# is used ONLY when the response is otherwise empty -- no content and
# no tool calls, the case the code below already reports as an empty
# message. A response with either is left exactly as it was, and the
# substitution is recorded in stderr so a caller reading the journal
# can tell an answer from a salvaged one.
reasoning_salvaged = False
if not content and not tool_calls:
reasoning = str(
message.get("reasoning_content") or message.get("reasoning") or ""
)
if reasoning.strip():
content = reasoning
reasoning_salvaged = True

if content:
transcript.append(content)

Expand Down Expand Up @@ -496,9 +529,16 @@ def _rate() -> dict[str, Any]:
# indistinguishable from one that actually said something, so a
# caller inspecting the result (or the harness_error/decompose
# rejection classifiers) can tell the two apart.
stderr = (
"" if stdout.strip() else "assistant returned an empty message with no tool calls"
)
if not stdout.strip():
stderr = "assistant returned an empty message with no tool calls"
elif reasoning_salvaged:
stderr = (
"assistant returned an empty `content`; answer salvaged from "
"`reasoning_content` -- treat this result as reasoning text, "
"not a composed answer"
)
else:
stderr = ""
return AgentResult(
exit_status=0,
stdout=stdout,
Expand Down
6 changes: 6 additions & 0 deletions src/jumar/verify/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ def verify_command(check: Check, ctx: VerifyContext) -> VerificationResult:
cwd=ctx.cwd,
capture_output=True,
timeout=check.timeout_s,
# No stdin. A check whose argv omits its file operand -- `grep -qE
# PATTERN` with no path -- otherwise inherits jumar's stdin and
# blocks on it until the timeout. Seen 6 Sep: 3 x 300s on one
# subtask, then repairs_exhausted, for a command that exits
# immediately against a closed stdin.
stdin=subprocess.DEVNULL,
shell=False,
check=False,
)
Expand Down
Loading