Skip to content

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

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

PratikDhanave (PratikDhanave) wants to merge 3 commits 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

This comment has been minimized.

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

@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the public-api-change Pull Request changes public APIs label Oct 3, 2026

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

Warning

Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • github.com
  • raw.githubusercontent.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "github.com"
    - "raw.githubusercontent.com"

See Network Configuration for more information.

Generated by Go API Consistency Review Agent for #1162 · copilot · auto · 236.9 AIC · ⌖ 8.05 AIC · ⊞ 13.9K

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

Judge is never asked for structured output, so the primary verdict path is effectively unreachable.

Upstream, both the .NET AIJudgeLoopEvaluator and Python AgentLoopMiddleware.with_judge explicitly request structured output from the judge client and only fall back to the VERDICT: DONE/VERDICT: MORE text markers when the client doesn't honor it:

  • .NET: this._judgeClient.GetResponseAsync<JudgeVerdict>(judgeMessages, LoopJsonContext.Default.Options, ...) (AIJudgeLoopEvaluator.cs)
  • Python: judge_client.get_response(judge_messages, options={"response_format": response_format}) with response_format: ... = JudgeVerdict (_loop.py)

Here, e.judge.Run(ctx, ..., agent.WithInstructions(e.instructions)) never passes agent.WithResponseFormat(...) (e.g. via jsonformat.MustFor[loop.JudgeVerdict](), the pattern used in examples/02-agents/agents/step05_structured_output). extractJudgeVerdict only succeeds by incidentally finding a {...} blob with an "answered" key in free-form prose — most judge models will instead follow the text-marker instructions literally, so the structured-output branch (and the richer GapAnalysis it carries) will rarely fire in practice, diverging from both upstream implementations' designed behavior.

Suggested fix: request structured output from the judge (e.g. agent.WithResponseFormat(jsonformat.MustFor[JudgeVerdict]()) or agent.WithStructuredOutput(&verdict)), falling back to extractJudgeVerdict/text markers only when the provider doesn't support it or decoding fails.

…evaluator

# Conflicts:
#	agent/harness/loop/loop_test.go
@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

API consistency review

Result Scope
aligned public API, user-visible behavior

Changed Go contract

New exported loop.AIJudgeEvaluator, NewAIJudgeEvaluator(judge *agent.Agent, AIJudgeConfig), AIJudgeConfig{Instructions, Criteria, FeedbackMessageTemplate}, JudgeVerdict{Answered, GapAnalysis}, the AIJudge*Marker and *Placeholder constants, and DefaultAIJudgeInstructions / DefaultAIJudgeFeedbackTemplate. The evaluator is opt-in; nothing uses a judge by default.

Upstream evidence reviewed

Implementation Source Contract checked
.NET AIJudgeLoopEvaluator.cs (AIJudgeLoopEvaluator, EvaluateAsync, RenderCriteria) Default instructions and feedback template text, the VERDICT: DONE / VERDICT: MORE markers with MORE winning, the {criteria} and {gap_analysis} placeholders, the <unknown> gap fallback, the request-contents framing, and the opt-in construction.
.NET AIJudgeLoopEvaluatorOptions.cs, JudgeVerdict.cs Option shape (Instructions, Criteria, FeedbackMessageTemplate) and the verdict type (answered, gapAnalysis).
Python Not checked I did not look for an equivalent Python evaluator.

Assessment

Constants, defaults, criteria rendering, feedback substitution and text-marker fallback match .NET, and both are opt-in. Two differences are idiomatic and not parity issues. Go takes a judge *agent.Agent with WithInstructions where .NET takes an IChatClient and sends a system message. Go also parses the verdict JSON out of the response text where .NET uses GetResponseAsync<JudgeVerdict>. Maintainers may want to confirm that the Go judge agent is meant to request structured output explicitly. The diff only instructs the judge to set answered, so it relies on the lenient text parse. public-api-change is already present.

Generated by Go API Consistency Review Agent · copilot · auto · 25.6 AIC · ⌖ 0.623 AIC · ⊞ 5.1K · ◷

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 parity-approved Go API consistency review found no parity issues public-api-change Pull Request changes public APIs 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