Fix: when agentic chat fail, show the real error (#21964)

This commit is contained in:
Wang Qi authored and GitHub committed 2026-11-04 17:01:53 +08:00
1 parent 1e3cb2bb29
commit 089e1ac242
2 files changed
+23 -6

No files matched your search

+21 -4
View File
@@ -553,6 +553,9 @@ func Run(ctx context.Context, in Input) (string, error) {
inputMessageIDs[adk.GetMessageID(msg)] = true
}
final, evidence, runErr := consumeAgentEvents(ctx, iter, in.OnDelta, in.ToolCallCounts, in.ToolCallErrors, in.ToolErrorSamples)
if runErr != nil {
runErr = fmt.Errorf("agentic research failed: %w", withCancellationCause(ctx, runErr))
}
// Recovery, cheapest and most faithful first. A run that ended on a
// narration tail or on a bare tool call still holds the model's own answer
@@ -586,7 +589,7 @@ func Run(ctx context.Context, in Input) (string, error) {
// would reintroduce the failure this call exists to survive. The
// unbounded-work risk that WithoutCancel normally carries is covered
// by the bounded budget attached here.
synthCtx, synthCancel := context.WithTimeout(context.WithoutCancel(ctx), finalizeTimeout)
synthCtx, synthCancel := context.WithTimeoutCause(context.WithoutCancel(ctx), finalizeTimeout, fmt.Errorf("final answer synthesis timed out after %s", finalizeTimeout))
defer synthCancel()
synthCtx = tokenizer.WithUsagePhase(synthCtx, "synth")
synthMessages := in.Messages
@@ -594,15 +597,17 @@ func Run(ctx context.Context, in Input) (string, error) {
synthMessages = append([]*schema.Message{schema.SystemMessage(tmpl.Content)}, synthMessages...)
}
synth, synthErr := finalizeAnswer(synthCtx, synthModelFor(in), synthMessages, final, evidence)
if synthErr == nil && strings.TrimSpace(synth) == "" {
synthErr = errors.New("empty answer")
}
if synthErr != nil {
synthErr = fmt.Errorf("final answer synthesis failed: %w", withCancellationCause(synthCtx, synthErr))
// Without this an empty final is completely silent: the run looks
// healthy in every other log line and the user simply gets nothing.
common.WarnCtx(ctx, "agentic_rag: finalizeAnswer failed", zap.Error(synthErr))
} else if strings.TrimSpace(synth) == "" {
common.WarnCtx(ctx, "agentic_rag: finalizeAnswer returned an empty answer")
}
switch {
case synthErr == nil && strings.TrimSpace(synth) != "":
case synthErr == nil:
// Replace rather than append: appending leaves the reader with a
// narration paragraph followed by the real answer. The single
// answer emission at the end of Run carries it.
@@ -611,6 +616,8 @@ func Run(ctx context.Context, in Input) (string, error) {
case runErr == nil:
// Normal termination but synthesis failed — surface the reason.
runErr = synthErr
default:
runErr = fmt.Errorf("%w; %w", runErr, synthErr)
}
}
@@ -622,6 +629,16 @@ func Run(ctx context.Context, in Input) (string, error) {
return final, runErr
}
// Eino wraps ctx.Err(), which omits cancellation causes such as token exhaustion.
func withCancellationCause(ctx context.Context, err error) error {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
if cause := context.Cause(ctx); cause != nil && !errors.Is(err, cause) {
return fmt.Errorf("%w: %w", cause, err)
}
}
return err
}
var modelFailureRe = regexp.MustCompile(`exceeds max retries|failed on every model|short-circuited by failover cooldown|overloaded_error|status 529|insufficient balance`)
// consumeAgentEvents drains the agent event iterator, streaming assistant
+2 -2
View File
@@ -107,7 +107,7 @@ func (s *ChatPipelineService) agenticRag(
go func() {
defer close(out)
runCtx, cancel := context.WithTimeout(ctx, agenticRunTimeout)
runCtx, cancel := context.WithTimeoutCause(ctx, agenticRunTimeout, fmt.Errorf("agentic run timed out after %s", agenticRunTimeout))
defer cancel()
// The run's evidence registry: every tool result stamps its passages with
// the [ID:n] handle the model cites, and buildAgenticReference resolves
@@ -319,7 +319,7 @@ func (s *ChatPipelineService) agenticRag(
zap.Any("tool_errors", toolErrors),
zap.String("error", runErr.Error()),
}, usageFields...)...)
emitResult(AsyncChatResult{Answer: fmt.Sprintf("**ERROR**: %s", runErr.Error()), Final: true})
emitResult(AsyncChatResult{Answer: fmt.Sprintf("**ERROR**: %s", runtime.MarkdownSafeErrorText(runErr)), Final: true})
return
}
common.InfoCtx(ctx, "agentic turn done", append([]zap.Field{