Skip to content

Commit 4800b6c

Browse files
committed
Merge remote-tracking branch 'origin/refactor' into feat/memory-read-only-cli
# Conflicts: # README.md # src/handlers/index.tsx # src/handlers/root.test.tsx # src/testing/TestCoreClient.tsx
2 parents 346e476 + 9505185 commit 4800b6c

82 files changed

Lines changed: 2278 additions & 67 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ agentcore # interactive TUI
6767
├── memory # inspect AgentCore Memories
6868
│ ├── get # fetch a Memory by id
6969
│ └── list # list Memories (server-side paginated)
70+
├── eval # evaluate and optimize AgentCore agents
71+
│ └── evaluator # manage AgentCore evaluators
72+
│ ├── llm-as-a-judge # LLM-as-a-Judge evaluators
73+
│ │ ├── create # create (instructions + rating scale + model)
74+
│ │ └── update # update (merged over the existing config)
75+
│ ├── code-based # code-based (Lambda-backed) evaluators
76+
│ │ ├── create # create (Lambda ARN + optional timeout)
77+
│ │ └── update # update (merged over the existing config)
78+
│ ├── get # get an evaluator by id (type-agnostic)
79+
│ ├── list # list evaluators (server-side paginated)
80+
│ └── delete # delete an evaluator by id
7081
└── config # read/write global config values
7182
```
7283

@@ -122,8 +133,35 @@ agentcore identity api-key-credential-provider get --name my-provider
122133
agentcore identity api-key-credential-provider list --max-results 10
123134
agentcore identity api-key-credential-provider update --name my-provider --api-key <new-key>
124135
agentcore identity api-key-credential-provider delete --name my-provider
136+
137+
# Manage evaluators
138+
# Create an LLM-as-a-Judge evaluator with a rating-scale preset.
139+
agentcore eval evaluator llm-as-a-judge create \
140+
--name order-support-quality \
141+
--level SESSION \
142+
--model us.anthropic.claude-sonnet-4-5-20250929-v1:0 \
143+
--instructions "Judge from {context} whether the order-support agent answered correctly." \
144+
--rating-scale 1-5-quality \
145+
--json
146+
147+
# Create a code-based (Lambda-backed) evaluator; timeout defaults to the service value.
148+
agentcore eval evaluator code-based create \
149+
--name refund-policy-compliance \
150+
--level SESSION \
151+
--lambda-arn arn:aws:lambda:us-west-2:123456789012:function:refund-policy \
152+
--json
153+
154+
# Get, list, delete.
155+
agentcore eval evaluator get --id <evaluatorId> --json
156+
agentcore eval evaluator list --max-results 20 --json
157+
agentcore eval evaluator delete --id <evaluatorId> --json
125158
```
126159

160+
Source-aware values: any field flag documented as such accepts the value inline,
161+
`file://<path>` to read it from a file, or `-` to read it from stdin (the AWS CLI
162+
`file://` convention). A command reads stdin from at most one flag. For example,
163+
`--instructions file://order-quality.txt` or `--instructions -`.
164+
127165
Bare Runtime branches and leaves require a TTY on stdin and stdout. Supplying
128166
operation flags runs the command headlessly, and `--json` always suppresses TUI
129167
rendering.

src/core/eval.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import {
2+
CreateEvaluatorCommand,
3+
DeleteEvaluatorCommand,
4+
GetEvaluatorCommand,
5+
ListEvaluatorsCommand,
6+
UpdateEvaluatorCommand,
7+
type CreateEvaluatorRequest,
8+
type CreateEvaluatorResponse,
9+
type DeleteEvaluatorResponse,
10+
type EvaluatorConfig,
11+
type GetEvaluatorResponse,
12+
type ListEvaluatorsResponse,
13+
type UpdateEvaluatorResponse,
14+
} from "@aws-sdk/client-bedrock-agentcore-control";
15+
import { InputValidationError } from "../errors";
16+
import type { CodeBasedUpdate, CoreEvalClient, LlmAsAJudgeUpdate } from "../handlers/eval/types";
17+
import type { AwsClients, CoreOptions } from "./types";
18+
import { toClientConfig } from "./utils";
19+
20+
export class EvalClient implements CoreEvalClient {
21+
constructor(private readonly clients: AwsClients) {}
22+
23+
async createEvaluator(
24+
request: CreateEvaluatorRequest,
25+
options: CoreOptions,
26+
): Promise<CreateEvaluatorResponse> {
27+
return this.clients.control(toClientConfig(options)).send(new CreateEvaluatorCommand(request));
28+
}
29+
30+
// updateLlmAsAJudgeEvaluator rebuilds the full llmAsAJudge config from the
31+
// current evaluator, overlays the provided fields, and sends it. UpdateEvaluator
32+
// replaces the entire evaluatorConfig union, and the llmAsAJudge arm requires
33+
// instructions + ratingScale + modelConfig together, so a partial update would
34+
// otherwise drop the fields the caller didn't pass.
35+
async updateLlmAsAJudgeEvaluator(
36+
id: string,
37+
update: LlmAsAJudgeUpdate,
38+
options: CoreOptions,
39+
): Promise<UpdateEvaluatorResponse> {
40+
const control = this.clients.control(toClientConfig(options));
41+
const current = await control.send(new GetEvaluatorCommand({ evaluatorId: id }));
42+
43+
// Reject a type mismatch before merging: UpdateEvaluator replaces the whole
44+
// evaluatorConfig union, so merging into the wrong arm would silently convert
45+
// a code-based evaluator into an LLM-as-a-Judge one.
46+
if (!current.evaluatorConfig || !("llmAsAJudge" in current.evaluatorConfig)) {
47+
throw new InputValidationError(`Evaluator "${id}" is not an LLM-as-a-Judge evaluator`, {
48+
meta: { evaluatorId: id },
49+
});
50+
}
51+
const existing = current.evaluatorConfig.llmAsAJudge;
52+
53+
const instructions = update.instructions ?? existing?.instructions;
54+
const ratingScale = update.ratingScale ?? existing?.ratingScale;
55+
// Preserve the existing Bedrock model config (inferenceConfig,
56+
// additionalModelRequestFields, ...) and override only the model id, so an
57+
// update that touches other fields does not drop model tuning.
58+
const existingModel =
59+
existing?.modelConfig && "bedrockEvaluatorModelConfig" in existing.modelConfig
60+
? existing.modelConfig.bedrockEvaluatorModelConfig
61+
: undefined;
62+
const modelId = update.model ?? existingModel?.modelId;
63+
64+
if (!instructions || !ratingScale || !modelId) {
65+
throw new InputValidationError(
66+
`Evaluator "${id}" is missing configuration required to update it: ` +
67+
`instructions, rating scale, and model are all required`,
68+
{ meta: { evaluatorId: id } },
69+
);
70+
}
71+
72+
const evaluatorConfig: EvaluatorConfig = {
73+
llmAsAJudge: {
74+
instructions,
75+
ratingScale,
76+
modelConfig: { bedrockEvaluatorModelConfig: { ...existingModel, modelId } },
77+
},
78+
};
79+
80+
return control.send(
81+
new UpdateEvaluatorCommand({
82+
evaluatorId: id,
83+
evaluatorConfig,
84+
kmsKeyArn: update.kmsKeyArn,
85+
clientToken: update.clientToken,
86+
}),
87+
);
88+
}
89+
90+
// updateCodeBasedEvaluator mirrors updateLlmAsAJudgeEvaluator: it merges the
91+
// provided lambda ARN / timeout over the current codeBased config so unset
92+
// fields are preserved across the union-replacing UpdateEvaluator call.
93+
async updateCodeBasedEvaluator(
94+
id: string,
95+
update: CodeBasedUpdate,
96+
options: CoreOptions,
97+
): Promise<UpdateEvaluatorResponse> {
98+
const control = this.clients.control(toClientConfig(options));
99+
const current = await control.send(new GetEvaluatorCommand({ evaluatorId: id }));
100+
101+
// Same union-replacement hazard as updateLlmAsAJudgeEvaluator: reject a type
102+
// mismatch instead of converting the evaluator to code-based.
103+
if (!current.evaluatorConfig || !("codeBased" in current.evaluatorConfig)) {
104+
throw new InputValidationError(`Evaluator "${id}" is not a code-based evaluator`, {
105+
meta: { evaluatorId: id },
106+
});
107+
}
108+
const existing = current.evaluatorConfig.codeBased;
109+
const existingLambda =
110+
existing && "lambdaConfig" in existing ? existing.lambdaConfig : undefined;
111+
112+
const lambdaArn = update.lambdaArn ?? existingLambda?.lambdaArn;
113+
if (!lambdaArn) {
114+
throw new InputValidationError(
115+
`Evaluator "${id}" is missing configuration required to update it: a Lambda ARN is required`,
116+
{ meta: { evaluatorId: id } },
117+
);
118+
}
119+
const lambdaTimeoutInSeconds = update.timeout ?? existingLambda?.lambdaTimeoutInSeconds;
120+
121+
const evaluatorConfig: EvaluatorConfig = {
122+
codeBased: { lambdaConfig: { ...existingLambda, lambdaArn, lambdaTimeoutInSeconds } },
123+
};
124+
125+
return control.send(
126+
new UpdateEvaluatorCommand({
127+
evaluatorId: id,
128+
evaluatorConfig,
129+
kmsKeyArn: update.kmsKeyArn,
130+
clientToken: update.clientToken,
131+
}),
132+
);
133+
}
134+
135+
async getEvaluator(id: string, options: CoreOptions): Promise<GetEvaluatorResponse> {
136+
return this.clients
137+
.control(toClientConfig(options))
138+
.send(new GetEvaluatorCommand({ evaluatorId: id }));
139+
}
140+
141+
async listEvaluators(
142+
nextToken: string | undefined,
143+
maxResults: number | undefined,
144+
options: CoreOptions,
145+
): Promise<ListEvaluatorsResponse> {
146+
return this.clients
147+
.control(toClientConfig(options))
148+
.send(new ListEvaluatorsCommand({ nextToken, maxResults }));
149+
}
150+
151+
async deleteEvaluator(id: string, options: CoreOptions): Promise<DeleteEvaluatorResponse> {
152+
return this.clients
153+
.control(toClientConfig(options))
154+
.send(new DeleteEvaluatorCommand({ evaluatorId: id }));
155+
}
156+
}

src/core/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BedrockAgentCoreControlClient } from "@aws-sdk/client-bedrock-agentcore-control";
22
import { BedrockAgentCoreClient } from "@aws-sdk/client-bedrock-agentcore";
33
import { IAMClient } from "@aws-sdk/client-iam";
4+
import { EvalClient } from "./eval";
45
import { HarnessClient } from "./harness";
56
import { IdentityClient } from "./identity";
67
import { MemoryClient } from "./memory";
@@ -50,6 +51,7 @@ export class CoreClient implements AwsClients {
5051
readonly identity: IdentityClient = new IdentityClient(this);
5152
readonly memory: MemoryClient = new MemoryClient(this);
5253
readonly runtime: RuntimeClient = new RuntimeClient(this);
54+
readonly eval: EvalClient = new EvalClient(this);
5355

5456
readonly projectManager: ProjectManager;
5557

src/handlers/config/config.test.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
55
import { createRootHandler } from "../index";
66
import { createSilentLogger, TestCoreClient, testIO } from "../../testing";
77
import { DefaultGlobalConfigAccessor } from "../../globalConfig";
8+
import { InputValidationError } from "../../errors";
89
import { FsReadWriteJson } from "../../io";
910

1011
describe("config", () => {
@@ -94,13 +95,11 @@ describe("config", () => {
9495
});
9596

9697
test("throws on invalid key", async () => {
97-
// TODO: swap to validation error.
98-
await expect(run(["nonexistent.key"])).rejects.toThrow(TypeError);
98+
await expect(run(["nonexistent.key"])).rejects.toThrow(InputValidationError);
9999
});
100100

101101
test("throws on invalid value for key", async () => {
102-
// TODO: swap to validation error
103-
await expect(run(["telemetry.enabled", "banana"])).rejects.toThrow(TypeError);
102+
await expect(run(["telemetry.enabled", "banana"])).rejects.toThrow(InputValidationError);
104103
});
105104

106105
test("coerces values based on schema", async () => {

src/handlers/config/handler.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import z from "zod";
22
import { createHandler, argument, GlobalConfigAccessorKey } from "../../router";
33
import { JsonRendererKey } from "../../tui";
4+
import { InputValidationError } from "../../errors";
45
import { DEFAULT_GLOBAL_CONFIG, type GlobalConfig } from "../../globalConfig";
56

67
/*
@@ -63,16 +64,14 @@ function coerceValue(current: unknown, raw: string, path: string): unknown {
6364
const normalized = raw.trim().toLowerCase();
6465
if (normalized === "true") return true;
6566
if (normalized === "false") return false;
66-
// TODO: mark as validation error.
67-
throw new TypeError(`Cannot coerce "${raw}" to boolean at "${path}"`);
67+
throw new InputValidationError(`Cannot coerce "${raw}" to boolean at "${path}"`);
6868
}
6969

7070
case "number": {
7171
const trimmed = raw.trim();
7272
const n = Number(trimmed);
7373
if (trimmed === "" || Number.isNaN(n)) {
74-
// TODO: mark as validation error.
75-
throw new TypeError(`Cannot coerce "${raw}" to number at "${path}"`);
74+
throw new InputValidationError(`Cannot coerce "${raw}" to number at "${path}"`);
7675
}
7776
return n;
7877
}
@@ -81,15 +80,14 @@ function coerceValue(current: unknown, raw: string, path: string): unknown {
8180
try {
8281
return JSON.parse(raw);
8382
} catch (e) {
84-
// TODO: mark as validation error.
85-
86-
throw new TypeError(`Cannot coerce "${raw}" to object at "${path}"`, { cause: e });
83+
throw new InputValidationError(`Cannot coerce "${raw}" to object at "${path}"`, {
84+
cause: e,
85+
});
8786
}
8887
}
8988

9089
default:
91-
// TODO: mark as validation error.
92-
throw new TypeError(`Unsupported target type "${typeof current}" at "${path}"`);
90+
throw new InputValidationError(`Unsupported target type "${typeof current}" at "${path}"`);
9391
}
9492
}
9593
/** Type guard that narrows `value` to a plain object record. */
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"evaluatorArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:evaluator/agentcore_cli_eval_fixture_llaj-IFK4y04Eae",
3+
"evaluatorId": "agentcore_cli_eval_fixture_llaj-IFK4y04Eae",
4+
"createdAt": {
5+
"$date": "2026-07-28T22:35:10.520Z"
6+
},
7+
"status": "ACTIVE"
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"evaluatorArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:evaluator/agentcore_cli_eval_fixture_code-KnZOVbDFFG",
3+
"evaluatorId": "agentcore_cli_eval_fixture_code-KnZOVbDFFG",
4+
"createdAt": {
5+
"$date": "2026-07-28T22:35:10.771Z"
6+
},
7+
"status": "ACTIVE"
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"evaluatorArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:evaluator/agentcore_cli_eval_fixture_tuned-oATpVrAtsP",
3+
"evaluatorId": "agentcore_cli_eval_fixture_tuned-oATpVrAtsP",
4+
"createdAt": {
5+
"$date": "2026-07-28T22:35:13.640Z"
6+
},
7+
"status": "ACTIVE"
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"evaluatorArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:evaluator/agentcore_cli_eval_fixture_tuned-oATpVrAtsP",
3+
"evaluatorId": "agentcore_cli_eval_fixture_tuned-oATpVrAtsP",
4+
"status": "DELETING"
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"evaluatorArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:evaluator/agentcore_cli_eval_fixture_code-KnZOVbDFFG",
3+
"evaluatorId": "agentcore_cli_eval_fixture_code-KnZOVbDFFG",
4+
"status": "DELETING"
5+
}

0 commit comments

Comments
 (0)