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

@jariy17jariy17 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 evaluateondemand evaluate (this PR)
SDK callStartBatchEvaluationEvaluate (data plane)
Trace gatheringservice-sideclient-side (CloudWatch Logs Insights)
Returnsjob 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 Nor--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-actionsgithub-actionsBot 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 linesPatch %Lines
src/core/eval.tsx95.39%10 Missing ⚠️
src/handlers/eval/ondemand/evaluate/index.tsx98.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-actionsgithub-actionsBot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@jariy17
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch 9 times, most recently from 23781e6 to 25228b3CompareAugust 12, 2026 20:49
@jariy17
jariy17 marked this pull request as ready for review August 12, 2026 21:17
Comment threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch from d1c79cf to 59e81ddCompareAugust 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 threadsrc/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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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

functionresolveWindow(flags: DataSourceFlags): SessionWindow|undefined{
?

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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:refactorAug 14, 2026
7 of 13 checks passed
Sign up for freeto 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

@jariy17@codecov-commenter@nborges-aws@Hweinstock@aidandaly24
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@jariy17jariy17 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 evaluateondemand evaluate (this PR)
SDK callStartBatchEvaluationEvaluate (data plane)
Trace gatheringservice-sideclient-side (CloudWatch Logs Insights)
Returnsjob 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 Nor--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-actionsgithub-actionsBot 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 linesPatch %Lines
src/core/eval.tsx95.39%10 Missing ⚠️
src/handlers/eval/ondemand/evaluate/index.tsx98.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-actionsgithub-actionsBot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@jariy17
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch 9 times, most recently from 23781e6 to 25228b3CompareAugust 12, 2026 20:49
@jariy17
jariy17 marked this pull request as ready for review August 12, 2026 21:17
Comment threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch from d1c79cf to 59e81ddCompareAugust 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 threadsrc/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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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

functionresolveWindow(flags: DataSourceFlags): SessionWindow|undefined{
?

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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:refactorAug 14, 2026
7 of 13 checks passed
Sign up for freeto 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

@jariy17@codecov-commenter@nborges-aws@Hweinstock@aidandaly24
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@jariy17jariy17 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 evaluateondemand evaluate (this PR)
SDK callStartBatchEvaluationEvaluate (data plane)
Trace gatheringservice-sideclient-side (CloudWatch Logs Insights)
Returnsjob 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 Nor--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-actionsgithub-actionsBot 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 linesPatch %Lines
src/core/eval.tsx95.39%10 Missing ⚠️
src/handlers/eval/ondemand/evaluate/index.tsx98.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-actionsgithub-actionsBot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@jariy17
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch 9 times, most recently from 23781e6 to 25228b3CompareAugust 12, 2026 20:49
@jariy17
jariy17 marked this pull request as ready for review August 12, 2026 21:17
Comment threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch from d1c79cf to 59e81ddCompareAugust 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 threadsrc/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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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

functionresolveWindow(flags: DataSourceFlags): SessionWindow|undefined{
?

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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:refactorAug 14, 2026
7 of 13 checks passed
Sign up for freeto 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

@jariy17@codecov-commenter@nborges-aws@Hweinstock@aidandaly24
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@jariy17jariy17 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 evaluateondemand evaluate (this PR)
SDK callStartBatchEvaluationEvaluate (data plane)
Trace gatheringservice-sideclient-side (CloudWatch Logs Insights)
Returnsjob 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 Nor--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-actionsgithub-actionsBot 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 linesPatch %Lines
src/core/eval.tsx95.39%10 Missing ⚠️
src/handlers/eval/ondemand/evaluate/index.tsx98.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-actionsgithub-actionsBot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@jariy17
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch 9 times, most recently from 23781e6 to 25228b3CompareAugust 12, 2026 20:49
@jariy17
jariy17 marked this pull request as ready for review August 12, 2026 21:17
Comment threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch from d1c79cf to 59e81ddCompareAugust 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 threadsrc/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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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

functionresolveWindow(flags: DataSourceFlags): SessionWindow|undefined{
?

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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:refactorAug 14, 2026
7 of 13 checks passed
Sign up for freeto 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

@jariy17@codecov-commenter@nborges-aws@Hweinstock@aidandaly24
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@jariy17jariy17 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 evaluateondemand evaluate (this PR)
SDK callStartBatchEvaluationEvaluate (data plane)
Trace gatheringservice-sideclient-side (CloudWatch Logs Insights)
Returnsjob 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 Nor--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-actionsgithub-actionsBot 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 linesPatch %Lines
src/core/eval.tsx95.39%10 Missing ⚠️
src/handlers/eval/ondemand/evaluate/index.tsx98.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-actionsgithub-actionsBot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@jariy17
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch 9 times, most recently from 23781e6 to 25228b3CompareAugust 12, 2026 20:49
@jariy17
jariy17 marked this pull request as ready for review August 12, 2026 21:17
Comment threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch from d1c79cf to 59e81ddCompareAugust 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 threadsrc/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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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

functionresolveWindow(flags: DataSourceFlags): SessionWindow|undefined{
?

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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:refactorAug 14, 2026
7 of 13 checks passed
Sign up for freeto 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

@jariy17@codecov-commenter@nborges-aws@Hweinstock@aidandaly24
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@jariy17jariy17 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 evaluateondemand evaluate (this PR)
SDK callStartBatchEvaluationEvaluate (data plane)
Trace gatheringservice-sideclient-side (CloudWatch Logs Insights)
Returnsjob 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 Nor--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-actionsgithub-actionsBot 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 linesPatch %Lines
src/core/eval.tsx95.39%10 Missing ⚠️
src/handlers/eval/ondemand/evaluate/index.tsx98.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-actionsgithub-actionsBot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@jariy17
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch 9 times, most recently from 23781e6 to 25228b3CompareAugust 12, 2026 20:49
@jariy17
jariy17 marked this pull request as ready for review August 12, 2026 21:17
Comment threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch from d1c79cf to 59e81ddCompareAugust 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 threadsrc/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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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

functionresolveWindow(flags: DataSourceFlags): SessionWindow|undefined{
?

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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:refactorAug 14, 2026
7 of 13 checks passed
Sign up for freeto 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

@jariy17@codecov-commenter@nborges-aws@Hweinstock@aidandaly24
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@jariy17jariy17 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 evaluateondemand evaluate (this PR)
SDK callStartBatchEvaluationEvaluate (data plane)
Trace gatheringservice-sideclient-side (CloudWatch Logs Insights)
Returnsjob 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 Nor--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-actionsgithub-actionsBot 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 linesPatch %Lines
src/core/eval.tsx95.39%10 Missing ⚠️
src/handlers/eval/ondemand/evaluate/index.tsx98.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-actionsgithub-actionsBot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@jariy17
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch 9 times, most recently from 23781e6 to 25228b3CompareAugust 12, 2026 20:49
@jariy17
jariy17 marked this pull request as ready for review August 12, 2026 21:17
Comment threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch from d1c79cf to 59e81ddCompareAugust 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 threadsrc/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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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

functionresolveWindow(flags: DataSourceFlags): SessionWindow|undefined{
?

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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:refactorAug 14, 2026
7 of 13 checks passed
Sign up for freeto 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

@jariy17@codecov-commenter@nborges-aws@Hweinstock@aidandaly24
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@jariy17jariy17 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 evaluateondemand evaluate (this PR)
SDK callStartBatchEvaluationEvaluate (data plane)
Trace gatheringservice-sideclient-side (CloudWatch Logs Insights)
Returnsjob 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 Nor--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-actionsgithub-actionsBot 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 linesPatch %Lines
src/core/eval.tsx95.39%10 Missing ⚠️
src/handlers/eval/ondemand/evaluate/index.tsx98.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-actionsgithub-actionsBot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Aug 12, 2026
@jariy17
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch 9 times, most recently from 23781e6 to 25228b3CompareAugust 12, 2026 20:49
@jariy17
jariy17 marked this pull request as ready for review August 12, 2026 21:17
Comment threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
jariy17force-pushed the feat/eval-ondemand-evaluate-clean branch from d1c79cf to 59e81ddCompareAugust 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 threadsrc/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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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 threadsrc/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
ContributorAuthor

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
ContributorAuthor

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

functionresolveWindow(flags: DataSourceFlags): SessionWindow|undefined{
?

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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:refactorAug 14, 2026
7 of 13 checks passed
Sign up for freeto 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

@jariy17@codecov-commenter@nborges-aws@Hweinstock@aidandaly24