Skip to content

[dotnet-port-api] Add AI-judge loop evaluator - #1162

Open
PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:feat/loop-ai-judge-evaluator
Open

PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:feat/loop-ai-judge-evaluator

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Ports the .NET AIJudgeLoopEvaluator (Harness/Loop). Go's loop harness previously shipped only CompletionMarkerEvaluator; the parity doc lists the AI-judge evaluator as a gap.

What it does

AIJudgeEvaluator queries a separate judge agent after each iteration to decide whether the user's original request has been fully addressed, continuing the loop (with the judge's gap analysis as feedback) while the answer is "no".

  • The judge sees the original request contents (framed by header text, so non-text content is preserved) and the agent's latest response, and is asked for a structured JudgeVerdict{answered, gapAnalysis}.
  • Fallback for judges that don't honor structured output: the non-overlapping VERDICT: DONE / VERDICT: MORE text markers, with MORE winning when the verdict is ambiguous or absent (so the loop keeps running rather than stopping on an incomplete answer) — matching .NET.
  • Criteria render into the judge instructions at {criteria}; the feedback template substitutes the gap analysis at {gap_analysis}.

Security

Mirrors the .NET type's security note in the doc comment: the judge is sent the request and latest response every iteration, so only use a judge you trust as much as the primary model, and prefer a stricter MaxIterations.

Tests

  • Structured verdict (answered → stop; not-answered → continue with gap in feedback).
  • Text-marker fallback (DONE → stop; MORE / ambiguous / absent → continue).
  • Criteria rendered into the judge instructions (blank entries skipped, placeholder removed).
  • Nil-judge panic.

Port the .NET AIJudgeLoopEvaluator: a loop evaluator that queries a separate
judge agent after each iteration to decide whether the user's original request
was fully addressed, continuing the loop with the judge's gap analysis as
feedback while the answer is "no".

The judge is queried with the original request contents and the agent's latest
response, and asked for a structured JudgeVerdict {answered, gapAnalysis}. When
the judge does not return structured output, the verdict falls back to the
non-overlapping VERDICT: DONE / VERDICT: MORE markers (MORE wins when ambiguous
or absent, so the loop keeps running). Criteria render into the judge
instructions at {criteria}, and the feedback template substitutes the gap
analysis at {gap_analysis}.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Three moderate evaluator issues must be addressed before approval.

Review effort: Lite
Findings: None

What changed in this PR

Adds a Go AIJudgeEvaluator that iteratively judges agent responses, supports criteria and gap-analysis feedback, and falls back to text verdict markers.

Changes:

  • Added evaluator configuration and verdict parsing.
  • Added criteria and feedback-template rendering.
  • Added structured, fallback, and nil-judge tests.
File Summary
agent/​harness/​loop/​loop_test.go Tests verdict handling, fallbacks, criteria, and nil judges.
agent/​harness/​loop/​evaluators.go Implements the evaluator. Three moderate findings remain: native structured output is not requested, non-text response content is omitted, and JSON extraction can reject valid verdicts surrounded by other JSON.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions github-actions Bot added kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure labels Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: public API, user-visible behavior

Changed Go contract: New exported agent/harness/loop API: AIJudgeEvaluator, NewAIJudgeEvaluator(judge *agent.Agent, config AIJudgeConfig), AIJudgeConfig{Instructions, Criteria, FeedbackMessageTemplate}, JudgeVerdict{Answered, GapAnalysis}, and constants AIJudgeDoneMarker/AIJudgeMoreMarker/AIJudgeCriteriaPlaceholder/AIJudgeGapAnalysisPlaceholder/DefaultAIJudgeInstructions/DefaultAIJudgeFeedbackTemplate.

Upstream evidence reviewed:

  • dotnet/src/Microsoft.Agents.AI/Harness/Loop/AIJudgeLoopEvaluator.cs (AIJudgeLoopEvaluator(IChatClient judgeClient, ...), EvaluateAsync, doc remarks "queried directly (without any agent tools, session, or middleware)")
  • dotnet/src/Microsoft.Agents.AI/Harness/Loop/AIJudgeLoopEvaluatorOptions.cs
  • dotnet/src/Microsoft.Agents.AI/Harness/Loop/JudgeVerdict.cs
  • dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/Loop/AIJudgeLoopEvaluatorTests.cs

Result: findings reported

Overall naming, defaults (opt-in only evaluator, MaxIterations guidance, non-overlapping DONE/MORE markers, {criteria}/{gap_analysis} placeholders, <unknown> gap-analysis fallback) all track the .NET port closely. Two parity gaps stood out, both in agent/harness/loop/evaluators.go:

  1. Judge isolation is not enforced. .NET's AIJudgeLoopEvaluator takes a raw IChatClient specifically so the judge call is "queried directly (without any agent tools, session, or middleware)" — a structural guarantee against the judge invoking tools or carrying session/history state. Go's NewAIJudgeEvaluator takes a full *agent.Agent, and Evaluate calls e.judge.Run(...) through the entire agent pipeline (default in-memory history provider, any configured ContextProviders, Tools, Middlewares). If a caller passes an agent configured with tools or history (an easy mistake since the API accepts any *agent.Agent), the judge could execute tools or accumulate state across loop iterations, diverging from the upstream isolation guarantee. Consider documenting this constraint explicitly and/or accepting a narrower abstraction (e.g., a raw provider/run function) instead of a full *agent.Agent.

  2. No native structured-output request. .NET calls judgeClient.GetResponseAsync<JudgeVerdict>(...), asking the provider for structured output natively before falling back to text markers only when the client doesn't honor it. Go's Evaluate never passes agent.WithStructuredOutput/agent.WithResponseFormat (both already exist in agent/options.go and agent/structuredoutput.go) — it only calls agent.WithInstructions(e.instructions) and then does manual {/} substring extraction from the free-text response. This means Go always relies on best-effort JSON scraping rather than requesting the provider's structured-output feature the way .NET does, which is a reliability/parity gap given the machinery already exists in the Go framework.

Neither issue blocks the port conceptually — the overall design, defaults, and text-fallback semantics match — but both reduce parity with the documented .NET behavior and should be considered before merge.

Generated by Go API Consistency Review Agent · copilot · auto · 153.5 AIC · ⌖ 7.82 AIC · ⊞ 9.2K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Generated by Go API Consistency Review Agent · copilot · auto · 153.5 AIC · ⌖ 7.82 AIC · ⊞ 9.2K

}
contents = append(contents, &message.TextContent{Text: "\n\n## Agent's latest response:\n" + loop.LastResponse.String()})

resp, err := e.judge.Run(ctx, []*message.Message{{Role: message.RoleUser, Contents: contents}}, agent.WithInstructions(e.instructions)).Collect()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Parity gap vs. the upstream .NET AIJudgeLoopEvaluator:

  1. No judge isolation. NewAIJudgeEvaluator takes a full *agent.Agent and Evaluate runs it through e.judge.Run(...), which goes through the entire agent pipeline (default in-memory history provider, any ContextProviders, Tools, Middlewares configured on that agent). Upstream's AIJudgeLoopEvaluator(IChatClient judgeClient, ...) intentionally takes a raw IChatClient so the judge is, per its doc remarks, "queried directly (without any agent tools, session, or middleware)". See dotnet/src/Microsoft.Agents.AI/Harness/Loop/AIJudgeLoopEvaluator.cs. If a caller passes a Go agent that has tools or session/history configured, the judge could execute tools or leak state across loop iterations — a structural guarantee the .NET type enforces at the type level that Go does not. Consider constraining the accepted judge type (e.g. a raw provider/run function) or explicitly documenting/enforcing the no-tools/no-session expectation.

  2. No native structured-output request. Upstream calls judgeClient.GetResponseAsync<JudgeVerdict>(judgeMessages, LoopJsonContext.Default.Options, ...), asking the provider for structured JSON output before falling back to text-marker parsing only when the client doesn't honor it (see TryGetResult branch in the same file). This Go Evaluate only passes agent.WithInstructions(e.instructions) and never uses agent.WithStructuredOutput/agent.WithResponseFormat (already available in agent/options.go / agent/structuredoutput.go), so it always falls back to manual {/} substring scraping (extractJudgeVerdict) rather than requesting the provider's native structured-output support like .NET does.

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

Labels

area:agent Changes files in the agent area kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure size:large At most 300 changed lines across at most 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants