feat(project): add project add memory - #2025
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## refactor #2025 +/- ##
============================================
+ Coverage 97.15% 97.17% +0.02%
============================================
Files 387 388 +1
Lines 23124 23360 +236
============================================
+ Hits 22465 22699 +234
- Misses 659 661 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
d114ff9 to
a332f9b
Compare
|
My agent ran focused testing for
The testing surfaced and we fixed empty shorthand entries, recursive unsupported-field stripping (including |
6d965bf to
4a06960
Compare
4a06960 to
3d10b6a
Compare
nborges-aws
left a comment
There was a problem hiding this comment.
LGTM thanks for updates!
Registers a `memory` leaf under `project add`, following the same SDK-union -> flat project-schema conversion pattern as `project add harness`. A memory scaffolds no files, so the command only appends an entry to `spec.memories` in agentcore.json; the L3 CDK turns that into an `AWS::BedrockAgentCore::Memory` at deploy time. Flags: --name, --event-expiry-duration, --strategies, --indexed-keys, --stream-delivery-resources, --encryption-key-arn, --execution-role-arn, --tags. --strategies accepts two forms: a comma-separated list of strategy types expanded with the CLI's default namespace templates, or a JSON MemoryStrategyInput[] mirroring the CreateMemory API for strategies that need explicit names, descriptions, or namespaces. clientToken is excluded (it is CreateMemory idempotency and this command makes no API call), and description is excluded until the L3 CDK schema supports it.
Stores an optional memory description in agentcore.json, matching the CreateMemory API's description field (max 4096 characters). The generated CDK app pins @aws/agentcore-cdk 0.1.0-alpha.45, whose MemorySchema is a non-strict z.object with no description field, so the key is stripped at synth rather than rejected until aws/agentcore-l3-cdk-constructs#325 ships and that pin is bumped. The flag help text says so.
The CDK's memory schema already models CUSTOM (@aws/agentcore-cdk
0.1.0-alpha.45 maps it to CFN customMemoryStrategy), so the CLI's four-type
enum was the outlier. A customMemoryStrategy in the --strategies JSON now
converts to { type: 'CUSTOM', name, description, namespaceTemplates }.
The shorthand form still takes managed types only: CUSTOM has no default
namespaces to expand. An extraction configuration or memoryRecordSchema is
rejected rather than dropped, since the CDK schema carries neither.
Also names the offending field in the memory validation error.
Reverts 87be86e. I added CUSTOM because the CDK schema already had it in MemoryStrategyTypeSchema, which turns out to be the argument PR aws#694 made -- and aws#713 reverted a day later. The CLI has removed CUSTOM twice on purpose. Offering the type without somewhere to put its extraction configuration is aws#241 ("select custom memory strategy, note there is no option to add prompts"); aws#266 removed it as a P0 to stop users picking an unsupported option, aws#694/aws#696 added it back with semanticOverride, and aws#713 reverted both as premature. aws#676 tracks doing it properly. The CDK keeping CUSTOM in its enum without a configuration field is the same hole, not a licence. So both forms are rejected again, now with an error that says why and points at aws#676. The one thing kept from the reverted commit: memory validation errors name the offending field, since issue.path was being dropped.
The one-line flag description is enough; the deploy-time caveat lives in the PR discussion rather than in help output.
Upstream moved the per-resource `project add` tests out of the monolithic project.test.ts into colocated add/<resource>/index.test.ts suites (harness in aws#2034, online-eval in aws#2048). Move the memory tests to match, with the same locally-duplicated run/inProject helpers those suites use. project.test.ts is now identical to upstream/refactor again, so this PR no longer touches it. Also drops the DeserializationError, FsReadWriteJson and ReadWriteJson imports, left dead there once the harness tests that used them moved to add/harness/index.test.ts. No test content changed: 187 project tests still pass, now across 10 files instead of 9.
3d10b6a to
1e902a7
Compare
| namespaceTemplates: z.array(z.string()).optional(), | ||
| }; | ||
|
|
||
| function projectMemoryObject<T extends z.ZodRawShape>(shape: T, label: string) { |
| }; | ||
|
|
||
| function projectMemoryObject<T extends z.ZodRawShape>(shape: T, label: string) { | ||
| const supportedFields = new Set(Object.keys(shape)); |
There was a problem hiding this comment.
I feel like there is a lot going on here just for parse and converting strategy inputs. Maybe we should create an adapter like this
xport interface MemoryStrategyAdapter<TInput = unknown> {
readonly type: MemoryStrategyType;
readonly memberKey: string;
readonly inputSchema: z.ZodType<TInput>;
fromInput(input: TInput): MemoryStrategy;
toInput(strategy: MemoryStrategy): TInput;
fromShorthand?(): MemoryStrategy;
canUseShorthand(strategy: MemoryStrategy): boolean;
}
The codec indexes registered adapters:
export class MemoryStrategyFlagCodec {
private readonly byType: Map<string, MemoryStrategyAdapter>;
private readonly byMember: Map<string, MemoryStrategyAdapter>;
constructor(adapters: readonly MemoryStrategyAdapter[]) {
this.byType = new Map(
adapters.map((adapter) => [adapter.type, adapter]),
);
this.byMember = new Map(
adapters.map((adapter) => [adapter.memberKey, adapter]),
);
}
parse(raw: string): MemoryStrategy[] {
return raw.trimStart().startsWith("[")
? this.parseJson(raw)
: this.parseShorthand(raw);
}
toFlag(strategies: readonly MemoryStrategy[]): string {
const adapters = strategies.map((strategy) => {
const adapter = this.byType.get(strategy.type);
if (!adapter) {
throw new InputValidationError(
`Unsupported memory strategy '${strategy.type}'`,
);
}
return adapter;
});
if (
strategies.every((strategy, index) =>
adapters[index].canUseShorthand(strategy),
)
) {
return strategies.map((strategy) => strategy.type).join(",");
}
return JSON.stringify(
strategies.map((strategy, index) => {
const adapter = adapters[index];
return {
[adapter.memberKey]: adapter.toInput(strategy),
};
}),
);
}
private parseShorthand(raw: string): MemoryStrategy[] {
return raw.split(",").map((value) => {
const type = value.trim();
const adapter = this.byType.get(type);
if (!adapter?.fromShorthand) {
throw new InputValidationError(
`Unsupported shorthand strategy '${type}'`,
);
}
return adapter.fromShorthand();
});
}
private parseJson(raw: string): MemoryStrategy[] {
const inputs = JSON.parse(raw) as unknown[];
return inputs.map((input) => this.parseJsonMember(input));
}
private parseJsonMember(input: unknown): MemoryStrategy {
// Validate that input is an object containing exactly one member.
const [memberKey] = Object.keys(input as object);
const adapter = this.byMember.get(memberKey);
if (!adapter) {
throw new InputValidationError(
`Unsupported memory strategy member '${memberKey}'`,
);
}
const value = adapter.inputSchema.parse(
(input as Record<string, unknown>)[memberKey],
);
return adapter.fromInput(value);
}
}
Registration is centralized:
const strategyCodec = new MemoryStrategyFlagCodec([
new StandardStrategyAdapter("SEMANTIC", "semanticMemoryStrategy"),
new StandardStrategyAdapter(
"SUMMARIZATION",
"summaryMemoryStrategy",
),
new StandardStrategyAdapter(
"USER_PREFERENCE",
"userPreferenceMemoryStrategy",
),
new EpisodicStrategyAdapter(),
]);
This PR adds
agentcore project add memory, which validates and appends a memory resource tospec.memoriesinagentcore.json. Memory resources do not scaffold any application files and are deployed through the generated CDK application.Example:
To maintain parity with the xisting CLI functionality, while also adding more configurability and customization,
--strategiessupports 2 input types:MemoryStrategyInput[]for explicit names, descriptions, and namespaces (there is a parameter help in--helpfor ease of understanding for the user)The command also supports expiry duration, indexed keys, stream delivery, encryption and execution roles, descriptions, and tags (description is a new optional field. Here is the CDK PR for it https://github.com/aws/agentcore-l3-cdk-constructs/pull/325 )
Callouts:
Validation
I tested it myself e2e with different combinations, flags, strategy conversions and validation failures. It all works well. I also tested it with
agetncore project buildTODO: check compatibility with
harnessresource