Skip to content

feat(eval): add ondemand evaluate (synchronous, client-side) - #1983

Merged
jariy17 merged 2 commits into
aws:refactorfrom
jariy17:feat/eval-ondemand-evaluate-clean
Aug 14, 2026
Merged

feat(eval): add ondemand evaluate (synchronous, client-side)#1983
jariy17 merged 2 commits into
aws:refactorfrom
jariy17:feat/eval-ondemand-evaluate-clean

Conversation

@jariy17

@jariy17 jariy17 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds agentcore eval ondemand evaluate — a client-side evaluation of existing sessions. On-demand gathers the sessions' traces from CloudWatch on the client, calls the Evaluate data-plane API directly, and prints scores.

batch-evaluation evaluate ondemand evaluate (this PR)
SDK call StartBatchEvaluation Evaluate (data plane)
Trace gathering service-side client-side (CloudWatch Logs Insights)
Returns job id (poll with get) scores, synchronously
Source arms --agent / --online-eval / --data-source-config --agent only

Usage

agentcore eval ondemand evaluate \
  --agent <harness-id|runtime-id> \
  --evaluator Builtin.Helpfulness \
  --session-ids <id...>            # or --lookback-days N, or --start-time/--end-time

Flags

  • --agent (required), --endpoint, --evaluator <ids...> (required)
  • time filter: --lookback-days N or --start-time/--end-time (ISO-8601, together)
  • --session-ids <ids...>, --trace-id <id> — independent, AND-ed fetch filters
  • --ground-truth <json> — inline / file:// / - → SDK-native EvaluationReferenceInput[]

Tests

  • Golden fixture suite (ondemand.fixture.test.tsx) — recorded GetAgentRuntime + Insights StartQuery/GetQueryResults (both log groups) + Evaluate fixtures, driven through the real root handler against a pinned window, diffed against evaluate.golden.json.
  • Command-flow suite (ondemand.test.tsx, TestCoreClient) — source-arm validation, getTracesForAgent → evaluate orchestration/order, --lookback-days window math, --trace-id, ground-truth passthrough.

tsc --noEmit, bun test src/ (1067 pass), oxlint, prettier — all clean.

@github-actions github-actions Bot added the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@codecov-commenter

codecov-commenter commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.41791% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.95%. Comparing base (4d183dc) to head (9b57081).

Files with missing lines Patch % Lines
src/core/eval.tsx 95.39% 10 Missing ⚠️
src/handlers/eval/ondemand/evaluate/index.tsx 98.16% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           refactor    #1983      +/-   ##
============================================
- Coverage     96.96%   96.95%   -0.01%     
============================================
  Files           364      366       +2     
  Lines         20758    21093     +335     
============================================
+ Hits          20127    20450     +323     
- Misses          631      643      +12     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@jariy17
jariy17 force-pushed the feat/eval-ondemand-evaluate-clean branch 9 times, most recently from 23781e6 to 25228b3 Compare August 12, 2026 20:49
@jariy17
jariy17 marked this pull request as ready for review August 12, 2026 21:17
Comment thread src/core/eval.tsx Outdated
const spanId = span.spanId;
if (typeof spanId !== "string" || spanId.length === 0) continue;
const attrs = span.attributes as Record<string, unknown> | undefined;
if (attrs?.["gen_ai.tool.name"] ?? attrs?.["tool.name"]) spanIds.push(spanId);

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.

Should tool-call selection use the standard operation/kind markers rather than requiring a tool-name attribute? Current telemetry identifies tool spans using gen_ai.operation.name === "execute_tool", openinference.span.kind === "TOOL", or traceloop.span.kind === "tool". The name fields are not always present. I reproduced a valid LangGraph-style tool span producing an empty toolCallSpanIds, so a TOOL_CALL evaluator makes no Evaluate request. Could we use the same marker logic as the evaluation SDK?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll add support to detect these "gen_ai.operation.name === "execute_tool", openinference.span.kind === "TOOL", or traceloop.span.kind === "tool"

Comment thread src/core/eval.tsx Outdated
try {
const evaluator = await control.send(new GetEvaluatorCommand({ evaluatorId: id }));
levels.set(id, evaluator.level ?? "SESSION");
} catch {

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.

Could evaluator lookup failures propagate instead of silently defaulting to SESSION? The level determines whether Evaluate receives traceIds, spanIds, or no target. I reproduced an AccessDeniedException here causing a trace evaluator to be submitted without an evaluation target, which can either evaluate the wrong scope or hide the actual permissions error. If a fallback is needed for compatibility, could it be limited to a missing level rather than every exception?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AWS SDK v3 responses have | undefined even if they are required. I'm just going to do ! because every evaluator must have a level.

Comment thread src/core/eval.tsx Outdated
}
}
}
return { sessionsEvaluated: input.traces.length, results };

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.

Could sessionsEvaluated count sessions for which an Evaluate request was actually sent? TRACE and TOOL_CALL sessions with no IDs are skipped above but still included here. I reproduced sessionsEvaluated: 1 with zero API calls and zero results. Tracking submitted sessions, or naming this sessionsDiscovered, would make the output less misleading.

// resolve inline / file:// / -, then hand the array to core verbatim — core
// groups it by session.
const resolver = new SourceResolver({ stdin: io.stdin });
const groundTruth = parseJsonFlag<EvaluationReferenceInput[]>(

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.

Can we change this to use parseJsonArrayFlag helper instead? Since groundTruth has to be an array.

@nborges-aws

nborges-aws commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Agree with Aidans findings + one additional comment

@jariy17
jariy17 force-pushed the feat/eval-ondemand-evaluate-clean branch from d1c79cf to 59e81dd Compare August 13, 2026 18:58
…d-evaluate-clean

# Conflicts:
#	src/core/eval.tsx
#	src/handlers/eval/index.tsx
#	src/handlers/eval/types.tsx
#	src/testing/TestCoreClient.tsx
Comment thread src/core/eval.tsx
// Sessions that actually produced Evaluate results — distinct from the sessions
// handed in, since a TRACE/TOOL_CALL session with no matching ids makes no call.
const evaluatedSessions = new Set<string>();
for (const evaluatorId of input.evaluatorIds) {

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.

nit: should we use levels.keys() here instead of the raw id array? Consistent with rest of code + leverages the dedup logic in resolveEvaluatorLevels(). A follow up item if you think its worth it

Comment thread src/core/eval.tsx
const logGroupName = runtimeLogGroup(runtimeId, qualifier);
const serviceName = runtimeServiceName(runtimeName, qualifier);

// CloudWatch Insights takes epoch seconds. Discovery defaults to now-7d when

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.

nit: i feel like the code explains this already.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in follow-up PR #2007: #2007

Comment thread src/core/eval.tsx
const [runtimeRows, sharedRows] = await Promise.all([
runInsightsQuery(logs, [logGroupName], queryString, startSec, endSec).catch((error) => {
if (error instanceof ResourceNotFoundException) {
throw new InputValidationError(

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.

should this be a different error type for telemetry? I wonder if it would be useful to distinguish invalid inputs from valid inputs without results.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was thinking we create a new Exception called TracesNotFound once we add observability to the cli.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in follow-up PR #2007: missing runtime telemetry now throws ResourceNotFoundError instead of InputValidationError, while preserving the CloudWatch exception as its cause. #2007

Comment thread src/core/eval.tsx
// (empty batch list); SESSION always makes one call with no target.
for (const target of targetBatches(level, trace)) {
const response = await data.send(
new EvaluateCommand({

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.

q: if a single evaluate fails, do we want to fail the entire run?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

On demand evaluate should be use for small evaluations (1-2 sessions with 2-3 evaluators) so this situation is unlikely to happen and also you could multiple evaluate calls per session if one of those fails, you have an incomplete evaluations

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, fail-fast is intentional for now. One session can require multiple Evaluate API calls across evaluators and target batches. Continuing after one fails could return incomplete results for that session while appearing successful. On-demand evaluation targets small synchronous runs; larger workloads should use batch evaluation. We can add explicit partial-failure handling later if customers need it.

Comment thread src/core/eval.tsx
}
return {
sessionsRequested: input.traces.length,
sessionsEvaluated: evaluatedSessions.size,

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.

q: I see we return the sessions evaluated and the results separately. Is there a use case for getting the results for a certain session or is it more useful in aggregate?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Results are already associated with sessions through context.spanContext.sessionId. Keeping them flat preserves the SDK response shape while still supporting per-session filtering and aggregate analysis. I also confirmed the session ID is present in a live on-demand evaluation response.

Comment thread src/core/eval.tsx
return value.replace(/'/g, "");
}

// buildSpanQuery is the single-phase Insights query: scope to one runtime by its

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.

nit: is there info in this comment not expressed by the code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in follow-up PR #2007: #2007

Comment thread src/core/eval.tsx
try {
doc = JSON.parse(message) as SpanRecord;
} catch {
continue;

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.

is it worth logging a warning here or would this be noisy?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in follow-up PR #2007 by passing the existing logger into the grouping helper and warning at most once when malformed telemetry records are skipped. #2007

groundTruth?: EvaluationReferenceInput[];
};

// EvaluateResult returns the raw Evaluate API results across all evaluators and

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.

nit: these comments describe usages of the type and feel like they could drift.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the usage-oriented type comments in follow-up PR #2007: #2007

// with start before end. On-demand owns this rather than reusing batch's resolver:
// batch has no --lookback-days and its window feeds a service-side data source, not
// a client-side Insights query.
function resolveWindow(flags: {

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.

how does this differ from

function resolveWindow(flags: DataSourceFlags): SessionWindow | undefined {
?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Batch's resolveWindow feeds the batch evaluation API's DataSourceConfig shape, which supports CloudWatch log groups and onlineEvaluationConfigArn.


// Record with: RECORD=1 bun test src/handlers/eval/ondemand/ondemand.fixture.test.tsx
//
// This exercises the real seam end to end: parsing → handler → CoreClient →

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.

nit: this feels overly verbose.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the verbose fixture comment block in follow-up PR #2007: #2007

).rejects.toThrow(/--agent/);
});

test("requires --evaluator", async () => {

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.

would these make sense as a test.each pattern for the different set of flags that reject?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Converted the repeated validation cases to test.each in follow-up PR #2007: #2007

@jariy17
jariy17 merged commit a9d34be into aws:refactor Aug 14, 2026
7 of 13 checks passed
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.

5 participants