Skip to content

feat(predict): add ReActV2 tool loop - #57

Open
isaacbmiller wants to merge 1 commit into
isaac/react-v2-pr5-native-adapter-controlsfrom
isaac/react-v2-pr6-reactv2-loop
Open

feat(predict): add ReActV2 tool loop#57
isaacbmiller wants to merge 1 commit into
isaac/react-v2-pr5-native-adapter-controlsfrom
isaac/react-v2-pr6-reactv2-loop

Conversation

@isaacbmiller

Copy link
Copy Markdown

Summary

  • add ReActV2 with a typed submit tool and native ToolCalls turn loop
  • record inputs, tool actions, observations, and final outputs in typed History
  • handle unknown tool names as error observations

Stack

  • Base PR: native adapter controls
  • Next PR: forced-submit fallback

Validation

@greptile-apps

greptile-apps Bot commented May 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces ReActV2, a new agent loop that replaces the text-based ReAct trace with a typed ToolCalls turn structure, a dedicated submit tool for final outputs, and structured History frames that record inputs, tool actions, observations, and completion state.

  • ReActV2 builds a submit tool from the signature's output fields and drives a max_iters loop via dspy.Predict, recording each turn in History; context-window overflow triggers a compaction attempt before breaking.
  • Tool observations (including error cases for unknown or failing tools) are stored as Observation objects with call_id linkage, enabling native tool-call message formatting downstream.

Confidence Score: 3/5

The 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 answer. Separately, a user-supplied tool named 'submit' is quietly discarded with no error or warning. Both issues affect real usage patterns and could surface as subtle, hard-to-diagnose bugs in downstream pipelines.

dspy/predict/reactv2.py — specifically the abnormal-exit return on line 129 and the tool registration block on lines 57-59.

Important Files Changed

Filename Overview
dspy/predict/reactv2.py New ReActV2 module implementing a typed submit-tool loop. Two logic issues: user-supplied tools named "submit" are silently overwritten, and abnormal-exit paths return a Prediction with no output fields. JSON schema type generation for parameterized generics also falls back to "string".
tests/predict/test_reactv2.py New test file covering submit, unknown-tool error handling, and observation ID recording. No tests for abnormal-exit paths (max_iters, context overflow, parse error) or the "submit" tool name collision.
dspy/predict/init.py Adds ReActV2 to the predict package exports — straightforward, no issues.

Sequence Diagram

sequenceDiagram
    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)"
Loading

Reviews (1): Last reviewed commit: "feat(predict): add ReActV2 tool loop" | Re-trigger Greptile

Comment thread dspy/predict/reactv2.py
Comment on lines +57 to +59
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment thread dspy/predict/reactv2.py
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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment thread dspy/predict/reactv2.py
Comment on lines +38 to +39
annotation = getattr(field, "annotation", str)
output_args[name] = {"type": _ANNOTATION_TO_JSON_TYPE.get(annotation, "string")}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 _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.

Suggested change
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")}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant