feat(predict): add ReActV2 tool loop - #57
Conversation
Greptile SummaryThis PR introduces
Confidence Score: 3/5The core loop and history recording work correctly for the happy path, but two logic gaps in the failure paths make this risky to merge without fixes. When the agent exhausts its iteration budget or hits a context/parse error, the returned Prediction carries no signature output fields — callers get silent None values for expected outputs like dspy/predict/reactv2.py — specifically the abnormal-exit return on line 129 and the tool registration block on lines 57-59. Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller
participant ReActV2
participant History
participant Predict
participant Tool
Caller->>ReActV2: "forward(**input_args)"
ReActV2->>History: append_input(input_args)
loop Each iteration (max_iters)
ReActV2->>Predict: "react(history, tools, **input_args)"
Predict-->>ReActV2: pred (next_thought, tool_calls)
ReActV2->>ReActV2: with_call_ids(prefix)
loop Each tool call
ReActV2->>Tool: "__call__(**args)"
Tool-->>ReActV2: ToolObservation
end
ReActV2->>History: append_outputs(thought+calls, observations)
alt submit call succeeded
ReActV2->>History: "append_output(obs.value, complete=True)"
ReActV2-->>Caller: "Prediction(termination_reason=submit, **outputs)"
end
end
ReActV2-->>Caller: "Prediction(termination_reason=break_reason)"
Reviews (1): Last reviewed commit: "feat(predict): add ReActV2 tool loop" | Re-trigger Greptile |
| tools = [tool if isinstance(tool, Tool) else Tool(tool) for tool in tools] | ||
| self.tools = {tool.name: tool for tool in tools} | ||
| self.tools["submit"] = _build_submit_tool(signature) |
There was a problem hiding this comment.
Silent "submit" tool name collision
If a caller passes a tool whose .name is "submit", it gets silently overwritten on the next line when the built-in submit tool is registered. The user's function is discarded with no warning or error, which is a hard-to-debug footgun. ReAct (the original) avoids this by not reserving any name, so this is a new invariant that should be enforced explicitly — either raise ValueError during __init__ or at least emit a warning via logger.warning.
| history.append_output(obs.value) | ||
| return dspy.Prediction(history=history, termination_reason="submit", **obs.value) | ||
|
|
||
| return dspy.Prediction(history=history, termination_reason=break_reason or "max_iters") |
There was a problem hiding this comment.
Abnormal-exit Prediction is missing signature output fields
When the loop exits without a successful submit call (via max_iters, context_overflow, no_tool_calls, or parse_error), the returned dspy.Prediction only carries history and termination_reason. None of the signature's declared output fields (e.g. answer) are present. dspy.Prediction silently returns None for missing keys, so callers that do result.answer will receive None with no indication that the agent never produced an answer. This is particularly dangerous in pipelines that pass the result downstream without checking termination_reason.
| annotation = getattr(field, "annotation", str) | ||
| output_args[name] = {"type": _ANNOTATION_TO_JSON_TYPE.get(annotation, "string")} |
There was a problem hiding this comment.
_ANNOTATION_TO_JSON_TYPE silently maps parameterized generics to "string"
The dict only keys on bare Python types (list, int, etc.). Parameterized generics like list[str], list[int], or str | None are not equal to those bare types, so _ANNOTATION_TO_JSON_TYPE.get(annotation, "string") returns "string" for them. A signature with answer: list[str] would advertise the submit tool's answer arg as type "string" to the LLM instead of "array", which can cause the model to format its response incorrectly.
| annotation = getattr(field, "annotation", str) | |
| output_args[name] = {"type": _ANNOTATION_TO_JSON_TYPE.get(annotation, "string")} | |
| annotation = getattr(field, "annotation", str) | |
| from typing import get_origin | |
| origin = get_origin(annotation) or annotation | |
| output_args[name] = {"type": _ANNOTATION_TO_JSON_TYPE.get(origin, "string")} |
Summary
ReActV2with a typed submit tool and nativeToolCallsturn loopHistoryStack
Validation
uv run --extra dev pytest -q tests/predict/test_reactv2.py