Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/core/eval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ export class EvalClient implements CoreEvalClient {
dataSourceConfig,
evaluationMetadata: input.groundTruth ? { sessionMetadata: input.groundTruth } : undefined,
kmsKeyArn: input.kmsKeyArn,
outputConfig: input.outputConfig,
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"batchEvaluationId": "golden_batch_evaluate_outputconfig1-936a801ab2",
"batchEvaluationArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:batch-evaluate/golden_batch_evaluate_outputconfig1-936a801ab2",
"batchEvaluationName": "golden_batch_evaluate_outputconfig1",
"status": "PENDING",
"createdAt": {
"$date": "2026-09-11T20:52:41.450Z"
},
"evaluators": [
{
"evaluatorId": "Builtin.Helpfulness"
}
],
"outputConfig": {
"cloudWatchConfig": {
"logStreamName": "run-golden_batch_evaluate_outputconfig1-936a801ab2",
"metricsNamespace": "Company/AgentEvaluations",
"resultDestination": "SOURCE_LOG_GROUP"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"batchEvaluationId": "golden_batch_simulate_endpoint1-120c119e26",
"batchEvaluationArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:batch-evaluate/golden_batch_simulate_endpoint1-120c119e26",
"batchEvaluationName": "golden_batch_simulate_endpoint1",
"status": "PENDING",
"createdAt": {
"$date": "2026-09-11T20:52:44.223Z"
},
"evaluators": [
{
"evaluatorId": "Builtin.Helpfulness"
}
],
"outputConfig": {
"cloudWatchConfig": {
"logStreamName": "run-golden_batch_simulate_endpoint1-120c119e26",
"metricsNamespace": "Bedrock-AgentCore/Evaluations",
"resultDestination": "SOURCE_LOG_GROUP"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"batchEvaluationId": "golden_batch_evaluate_outputconfig1-936a801ab2",
"batchEvaluationArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:batch-evaluate/golden_batch_evaluate_outputconfig1-936a801ab2",
"batchEvaluationName": "golden_batch_evaluate_outputconfig1",
"status": "PENDING",
"createdAt": "2026-09-11T20:52:41.450Z",
"evaluators": [
{
"evaluatorId": "Builtin.Helpfulness"
}
],
"outputConfig": {
"cloudWatchConfig": {
"logStreamName": "run-golden_batch_evaluate_outputconfig1-936a801ab2",
"metricsNamespace": "Company/AgentEvaluations",
"resultDestination": "SOURCE_LOG_GROUP"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"batchEvaluationId": "golden_batch_simulate_endpoint1-120c119e26",
"status": "PENDING",
"examplesInvoked": 1,
"examplesFailed": 0,
"sessions": [
{
"exampleId": "e1",
"sessionId": "00000000-0000-4000-8000-000000000001"
}
],
"failures": []
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const FIXTURE_EVAL_AGENT = "asdf_MyAgent-3s5axvBC6Q";
const FIXTURE_EVAL_NAME = "golden_batch_evaluate_fixture685";

const FIXTURE_SIMULATE_NAME = "golden_batch_simulate_fixture1";
const FIXTURE_OUTPUT_CONFIG_NAME = "golden_batch_evaluate_outputconfig1";
const FIXTURE_SIMULATE_ENDPOINT_NAME = "golden_batch_simulate_endpoint1";
const FIXTURE_SIMULATE_DATASET = join(FIXTURES, "simulate-ds.jsonl");

function createFixtureCore(): CoreClient {
Expand Down Expand Up @@ -185,4 +187,72 @@ describe("eval batch-evaluation (fixture-backed)", () => {

matchGolden(FIXTURES, "simulate.golden.json", io.stdout());
}, 180_000);

test("evaluate submits a job with a customer-supplied output config", async () => {
const stdout = await run([
"eval",
"batch-evaluation",
"evaluate",
"--agent",
FIXTURE_EVAL_AGENT,
"--evaluators",
"Builtin.Helpfulness",
"--name",
FIXTURE_OUTPUT_CONFIG_NAME,
"--output-config",
'{"cloudWatchConfig":{"resultDestination":"SOURCE_LOG_GROUP","metricsNamespace":"Company/AgentEvaluations"}}',
"--json",
]);

matchGolden(FIXTURES, "evaluate-output-config.golden.json", stdout);
});

test("simulate passes --endpoint through and submits with an output config", async () => {
let n = 0;
const { createControlClient, createDataClient, createIamClient, createLogsClient } =
fixtureFactories(FIXTURES);
const core = new CoreClient({
createControlClient,
createDataClient,
createIamClient,
createLogsClient,
logger: createSilentLogger(),
newSessionId: () => `00000000-0000-4000-8000-${String(++n).padStart(12, "0")}`,
});
const io = testIO();
const root = createRootHandler(core, {
io: io.io,
logger: createSilentLogger(),
globalConfigAccessor: new TestGlobalConfigAccessor(),
});

await root.route([
"node",
"agentcore",
"eval",
"batch-evaluation",
"simulate",
"--runtime-id",
FIXTURE_EVAL_AGENT,
"--endpoint",
"DEFAULT",
"--payload-template",
'{"prompt":"{input}"}',
"--dataset",
FIXTURE_SIMULATE_DATASET,
"--evaluators",
"Builtin.Helpfulness",
"--name",
FIXTURE_SIMULATE_ENDPOINT_NAME,
"--output-config",
'{"cloudWatchConfig":{"resultDestination":"SOURCE_LOG_GROUP"}}',
"--ingestion-wait-ms",
"0",
"--json",
"--region",
REGION,
]);

matchGolden(FIXTURES, "simulate-endpoint-output-config.golden.json", io.stdout());
}, 180_000);
});
91 changes: 89 additions & 2 deletions src/handlers/eval/batch-evaluation/batch-evaluation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ const RESULTS: BatchEvaluationResultEntry[] = [
{ evaluatorId: "Builtin.Helpfulness", level: "Session", sessionId: "s1", score: 5 },
];

async function run(args: string[], configure?: (core: TestCoreClient) => void) {
async function run(
args: string[],
configure?: (core: TestCoreClient) => void,
ioOptions?: { stdin?: string },
) {
const core = new TestCoreClient();
configure?.(core);
const io = testIO();
const io = testIO(ioOptions);
const root = createRootHandler(core, {
io: io.io,
logger: createSilentLogger(),
Expand Down Expand Up @@ -262,3 +266,86 @@ describe("eval batch-evaluation simulate", () => {
expect(JSON.parse(stdout).failures).toEqual([{ exampleId: "bad", error: "HTTP 500" }]);
});
});

describe("eval batch-evaluation evaluate --output-config", () => {
const BASE = [
"eval",
"batch-evaluation",
"evaluate",
"--agent",
"r-1",
"--evaluators",
"Builtin.Helpfulness",
"--name",
"eval-1",
];
test("rejects malformed JSON before any SDK call", async () => {
const core = new TestCoreClient();
const io = testIO();
const root = createRootHandler(core, {
io: io.io,
logger: createSilentLogger(),
globalConfigAccessor: new TestGlobalConfigAccessor(),
});
await expect(
root.route([
"node",
"agentcore",
...BASE,
"--output-config",
"{not json",
"--region",
"us-west-2",
]),
).rejects.toThrow(/Invalid JSON for option '--output-config'/);
expect(core.eval.calls).toEqual([]);
});
});

describe("eval batch-evaluation simulate --endpoint and --output-config", () => {
const BASE = [
"eval",
"batch-evaluation",
"simulate",
"--runtime-id",
"r-1",
"--payload-template",
'{"prompt":"{input}"}',
"--dataset",
"/tmp/ds.jsonl",
"--evaluators",
"Builtin.Helpfulness",
"--name",
"sim-1",
];
const invoked = (c: TestCoreClient) =>
c.eval.setInvokeDatasetResponse({
sessions: [{ exampleId: "e1", sessionId: "s1" }],
invoked: 1,
failed: 0,
failures: [],
});

test("malformed --output-config aborts before any Runtime is invoked", async () => {
const core = new TestCoreClient();
invoked(core);
const io = testIO();
const root = createRootHandler(core, {
io: io.io,
logger: createSilentLogger(),
globalConfigAccessor: new TestGlobalConfigAccessor(),
});
await expect(
root.route([
"node",
"agentcore",
...BASE,
"--output-config",
"{not json",
"--region",
"us-west-2",
]),
).rejects.toThrow(/Invalid JSON for option '--output-config'/);
expect(core.eval.calls.map((c) => c.method)).not.toContain("invokeDataset");
});
});
5 changes: 5 additions & 0 deletions src/handlers/eval/batch-evaluation/evaluate/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Core } from "../../../types";
import type { SessionMetadataShape } from "@aws-sdk/client-bedrock-agentcore";
import { coreOptsFromCtx, parseJsonFlag } from "../../../utils";
import { SessionSource } from "../../sessionSource";
import { BatchOutputConfig } from "../outputConfig";

const CONFIGURATION = "Configuration:";
const EVALUATION = "Evaluation:";
Expand Down Expand Up @@ -75,6 +76,7 @@ export const createEvaluateBatchEvaluationHandler = (core: Core, io: AppIO) =>
z.string().optional(),
{ group: EVALUATION, help: groundTruthHelp },
),
...BatchOutputConfig.flags,
],
handle: async (ctx, flags) => {
if (!flags["name"]) {
Expand All @@ -94,6 +96,8 @@ export const createEvaluateBatchEvaluationHandler = (core: Core, io: AppIO) =>
await resolver.resolveText("ground-truth", flags["ground-truth"]),
);

const outputConfig = await BatchOutputConfig.resolve(flags["output-config"], io);

const response = await core.eval.startBatchEvaluation(
{
name: flags["name"],
Expand All @@ -102,6 +106,7 @@ export const createEvaluateBatchEvaluationHandler = (core: Core, io: AppIO) =>
source,
groundTruth,
kmsKeyArn: flags["kms-key-arn"],
outputConfig,
},
coreOptsFromCtx(ctx),
);
Expand Down
54 changes: 54 additions & 0 deletions src/handlers/eval/batch-evaluation/outputConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { OutputConfig } from "@aws-sdk/client-bedrock-agentcore";
import z from "zod";
import { SourceResolver, type AppIO } from "../../../io";
import { flag } from "../../../router";
import { parseJsonFlag } from "../../utils";

const outputConfigHelp = `(JSON: tagged union object)
Where evaluation results and metrics are written. Omit it and results go to the
service-managed default location. Only top-level key: cloudWatchConfig.

Accepts inline JSON, file://<path>, or - to read stdin.

JSON syntax:
{
"cloudWatchConfig": {
"logGroupName": "string", // result log group; omit for
// SOURCE_LOG_GROUP, and it cannot sit
// under /aws/bedrock-agentcore/evaluations/
"logStreamName": "string", // result log stream
"metricsNamespace": "string", // defaults to
// Bedrock-AgentCore/Evaluations; cannot
// begin with "AWS/"
"resultDestination": "DEDICATED_LOG_GROUP" | "SOURCE_LOG_GROUP"
// DEDICATED_LOG_GROUP (default) writes to
// a dedicated result group;
// SOURCE_LOG_GROUP writes back to the log
// group the traces were read from
}
}

API reference:
https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_OutputConfig.html

Example:
--output-config '{"cloudWatchConfig":{"logGroupName":"/company/agent-evaluations","metricsNamespace":"Company/AgentEvaluations","resultDestination":"DEDICATED_LOG_GROUP"}}'`;

export class BatchOutputConfig {
static readonly flags = [
flag(
"output-config",
"where results and metrics are written (JSON OutputConfig)",
z.string().optional(),
{ group: "Result output:", help: outputConfigHelp },
),
] as const;

static async resolve(value: string | undefined, io: AppIO): Promise<OutputConfig | undefined> {
const resolver = new SourceResolver({ stdin: io.stdin });

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.

SourceResolver owns the one-stdin-option guard, but this creates a new instance separate from the resolvers used by SessionSource and --ground-truth in evaluate. With --ground-truth - --output-config - (or --data-source-config - --output-config -), the first option drains stdin and this one receives an empty string instead of reporting the conflict. Could we share one resolver or prevalidate that only one option uses stdin, and add a regression test?

return parseJsonFlag<OutputConfig>(
"output-config",
await resolver.resolveText("output-config", value),
);
}
}
Loading
Loading