fix(go-agent): route graph step errors through exception outputs (#17167)

## Summary

- Route ReAct graph step-limit errors through the Agent `_ERROR` output.
- Align the Go Agent default max rounds with Python by using five
rounds.
- Preserve cancellation, timeout, and configuration errors as real
failures.

## Testing

- `bash build.sh --test ./internal/agent/component/...`

<img width="2050" height="1409" alt="image"
src="https://github.com/user-attachments/assets/108e2d27-e115-4595-91a2-21f90c2531ae"
/>
This commit is contained in:
Hz_
2026-07-22 19:00:53 +08:00
committed by GitHub
parent ffccf0b0fa
commit 45ca5f9a3d
2 changed files with 31 additions and 7 deletions

View File

@@ -538,7 +538,8 @@ func buildAgentTools(p AgentParam) ([]einotool.BaseTool, error) {
// NewAgentComponent builds an AgentComponent from raw params.
func NewAgentComponent(p AgentParam) *AgentComponent {
if p.MaxRounds <= 0 {
p.MaxRounds = 3
// Keep the Python Agent default (AgentParam.max_rounds = 5).
p.MaxRounds = 5
}
return &AgentComponent{param: p}
}
@@ -627,7 +628,15 @@ func (c *AgentComponent) Invoke(ctx context.Context, inputs map[string]any) (map
}
}
if err != nil {
return nil, fmt.Errorf("component: Agent.Invoke: %w", err)
// Python's LLM layer turns model/tool execution failures into an
// ``**ERROR**`` response, which Agent exposes as the `_ERROR`
// component output. Preserve cancellation as a real graph error,
// but keep ordinary ReAct failures in the component data flow so
// Canvas exception branches can handle them.
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || !isAgentGraphRunError(err) {
return nil, fmt.Errorf("component: Agent.Invoke: %w", err)
}
return map[string]any{"_ERROR": "**ERROR**: " + err.Error()}, nil
}
// Post-stream citation grounding. When Cite is enabled and
// the canvas state has recorded retrieval chunks (populated
@@ -686,6 +695,18 @@ func (c *AgentComponent) Invoke(ctx context.Context, inputs map[string]any) (map
return out, nil
}
// isAgentGraphRunError identifies ReAct graph failures that Python exposes
// through the Agent component's _ERROR output. Construction/configuration
// errors must remain real component errors so invalid canvases still fail
// fast; only the graph execution limit/error is routed to exception branches.
func isAgentGraphRunError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "[graphrunerror]") || strings.Contains(msg, "exceeds max steps")
}
// Stream implements Component.Stream. Mirrors Invoke then pushes the
// single payload through the channel.
func (c *AgentComponent) Stream(ctx context.Context, inputs map[string]any) (<-chan map[string]any, error) {

View File

@@ -8,7 +8,7 @@
// 2. ToolCallRound: the runner returns a message with ToolCalls →
// component extracts them into the tool_calls output.
// 3. ExhaustRoundsError: the runner returns an error → component
// propagates it.
// exposes it through the Python-compatible _ERROR output.
// 4. MissingModelID: the component rejects before calling the runner.
package component
@@ -324,15 +324,18 @@ func TestAgent_ToolCallRound(t *testing.T) {
func TestAgent_ExhaustRoundsError(t *testing.T) {
withAgentRunner(t, func(_ context.Context, _ AgentParam) (*schema.Message, error) {
return nil, errors.New("agent: exhausted rounds without final answer")
return nil, errors.New("[GraphRunError] exceeds max steps")
})
c := NewAgentComponent(AgentParam{ModelID: "stub", MaxRounds: 2})
_, err := c.Invoke(context.Background(), map[string]any{
out, err := c.Invoke(context.Background(), map[string]any{
"user_prompt": "x",
})
if err == nil {
t.Fatal("expected error when loop exhausts without a final answer")
if err != nil {
t.Fatalf("Invoke: %v", err)
}
if got := out["_ERROR"]; got != "**ERROR**: [GraphRunError] exceeds max steps" {
t.Fatalf("_ERROR = %v, want Python-compatible error envelope", got)
}
}