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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ConfigIO } from '../../../../../../lib';
import type { AgentCoreProjectSpec, AwsDeploymentTarget, DeployedState } from '../../../../../../schema';
import type { AgentCoreProjectSpec, AwsDeploymentTarget, DeployedState, Memory } from '../../../../../../schema';
import * as harnessApi from '../../../../../aws/agentcore-harness';
import type { ImperativeDeployContext } from '../../types';
import { HarnessDeployer } from '../harness-deployer';
Expand Down Expand Up @@ -35,6 +35,7 @@ const CONFIG_ROOT = '/project/agentcore';

function createContext(overrides?: {
harnesses?: AgentCoreProjectSpec['harnesses'];
memories?: Memory[];
deployedHarnesses?: DeployedState['targets'][string]['resources'];
cdkOutputs?: Record<string, string>;
}): ImperativeDeployContext {
Expand All @@ -43,7 +44,7 @@ function createContext(overrides?: {
version: 1,
managedBy: 'CDK' as const,
runtimes: [],
memories: [],
memories: overrides?.memories ?? [],
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
Expand Down Expand Up @@ -535,6 +536,74 @@ describe('HarnessDeployer', () => {
});
});

describe('memorySpec resolution', () => {
const ROLE_ARN = 'arn:aws:iam::123456789012:role/HarnessRole';
const MEMORY_ARN = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123';
const CDK_OUTPUTS = { ApplicationHarnessMyHarnessRoleArnOutput123: ROLE_ARN };
const READY_HARNESS = {
harnessId: 'h-new',
harnessName: 'my_harness',
arn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:harness/h-new',
status: 'READY' as const,
executionRoleArn: ROLE_ARN,
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
};

const HARNESS_SPEC_WITH_MEMORY_ARN_JSON = JSON.stringify({
name: 'my_harness',
model: { provider: 'bedrock', modelId: 'anthropic.claude-3-sonnet-20240229-v1:0' },
tools: [],
skills: [],
memory: { arn: MEMORY_ARN },
});

it('resolves memorySpec by deployed ARN when memory.name is absent', async () => {
const memory: Memory = {
name: 'my_memory',
eventExpiryDuration: 30,
strategies: [{ type: 'SEMANTIC', namespaces: ['/users/{actorId}/facts'] }],
};

const ctx = createContext({
harnesses: [{ name: 'my_harness', path: 'harnesses/my_harness' }],
memories: [memory],
deployedHarnesses: {
memories: { my_memory: { memoryId: 'mem-123', memoryArn: MEMORY_ARN } },
},
cdkOutputs: CDK_OUTPUTS,
});

mockedReadFile
.mockResolvedValueOnce(HARNESS_SPEC_WITH_MEMORY_ARN_JSON)
.mockRejectedValueOnce(new Error('ENOENT'));
mockedMapHarness.mockResolvedValueOnce({ region: REGION, harnessName: 'my_harness', executionRoleArn: ROLE_ARN });
mockedCreateHarness.mockResolvedValueOnce({ harness: READY_HARNESS });

await deployer.deploy(ctx);

expect(mockedMapHarness).toHaveBeenCalledWith(expect.objectContaining({ memorySpec: memory }));
});

it('returns undefined memorySpec for a fully external ARN not in deployedResources', async () => {
const ctx = createContext({
harnesses: [{ name: 'my_harness', path: 'harnesses/my_harness' }],
memories: [],
cdkOutputs: CDK_OUTPUTS,
});

mockedReadFile
.mockResolvedValueOnce(HARNESS_SPEC_WITH_MEMORY_ARN_JSON)
.mockRejectedValueOnce(new Error('ENOENT'));
mockedMapHarness.mockResolvedValueOnce({ region: REGION, harnessName: 'my_harness', executionRoleArn: ROLE_ARN });
mockedCreateHarness.mockResolvedValueOnce({ harness: READY_HARNESS });

await deployer.deploy(ctx);

expect(mockedMapHarness).toHaveBeenCalledWith(expect.objectContaining({ memorySpec: undefined }));
});
});

describe('configHash', () => {
const ROLE_ARN = 'arn:aws:iam::123456789012:role/HarnessRole';
const CDK_OUTPUTS = { ApplicationHarnessMyHarnessRoleArnOutput123: ROLE_ARN };
Expand Down Expand Up @@ -659,6 +728,72 @@ describe('HarnessDeployer', () => {
);
expect(dockerfileCallArgs).toBeUndefined();
});

it('triggers update when memory strategy namespaces change', async () => {
const HARNESS_SPEC_WITH_MEMORY_JSON = JSON.stringify({
name: 'my_harness',
model: { provider: 'bedrock', modelId: 'anthropic.claude-3-sonnet-20240229-v1:0' },
tools: [],
skills: [],
memory: { name: 'my_memory' },
});

const memoryV1: Memory = {
name: 'my_memory',
eventExpiryDuration: 30,
strategies: [{ type: 'SEMANTIC', namespaces: ['/users/{actorId}/v1'] }],
};

// First deploy — capture hash for memoryV1
const ctxV1 = createContext({
harnesses: [{ name: 'my_harness', path: 'harnesses/my_harness' }],
memories: [memoryV1],
cdkOutputs: CDK_OUTPUTS,
});

mockedReadFile.mockResolvedValueOnce(HARNESS_SPEC_WITH_MEMORY_JSON).mockRejectedValueOnce(new Error('ENOENT'));
mockedMapHarness.mockResolvedValueOnce({ region: REGION, harnessName: 'my_harness', executionRoleArn: ROLE_ARN });
mockedCreateHarness.mockResolvedValueOnce({ harness: READY_HARNESS });

const result1 = await deployer.deploy(ctxV1);
const hashV1 = result1.state!.my_harness!.configHash;

vi.clearAllMocks();

// Second deploy — only the namespace in memoryV1 changes, harness.json is identical
const memoryV2: Memory = {
name: 'my_memory',
eventExpiryDuration: 30,
strategies: [{ type: 'SEMANTIC', namespaces: ['/users/{actorId}/v2'] }],
};

const ctxV2 = createContext({
harnesses: [{ name: 'my_harness', path: 'harnesses/my_harness' }],
memories: [memoryV2],
deployedHarnesses: {
harnesses: {
my_harness: {
harnessId: 'h-new',
harnessArn: READY_HARNESS.arn,
roleArn: ROLE_ARN,
status: 'READY',
configHash: hashV1,
},
},
},
cdkOutputs: CDK_OUTPUTS,
});

mockedReadFile.mockResolvedValueOnce(HARNESS_SPEC_WITH_MEMORY_JSON).mockRejectedValueOnce(new Error('ENOENT'));
mockedMapHarness.mockResolvedValueOnce({ region: REGION, harnessName: 'my_harness', executionRoleArn: ROLE_ARN });
mockedUpdateHarness.mockResolvedValueOnce({ harness: READY_HARNESS });

const result2 = await deployer.deploy(ctxV2);

expect(result2.state!.my_harness!.configHash).not.toBe(hashV1);
expect(mockedUpdateHarness).toHaveBeenCalled();
expect(result2.notes).toContain('Updated harness "my_harness"');
});
});

describe('teardown', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DeployedResourceState, HarnessSpec } from '../../../../../../schema';
import type { DeployedResourceState, HarnessSpec, Memory } from '../../../../../../schema';
import { mapHarnessSpecToCreateOptions } from '../harness-mapper';
import { readFile, stat } from 'fs/promises';
import { join } from 'path';
Expand Down Expand Up @@ -406,6 +406,201 @@ describe('mapHarnessSpecToCreateOptions', () => {
'Memory "nonexistent" referenced by harness is not in deployed state'
);
});

it('includes retrievalConfig derived from memory strategy namespaces', async () => {
const deployedResources: DeployedResourceState = {
memories: {
my_memory: {
memoryId: 'mem-123',
memoryArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123',
},
},
};
const memorySpec: Memory = {
name: 'my_memory',
eventExpiryDuration: 30,
strategies: [
{ type: 'SEMANTIC', namespaces: ['/users/{actorId}/facts'] },
{ type: 'USER_PREFERENCE', namespaces: ['/users/{actorId}/preferences'] },
{ type: 'SUMMARIZATION', namespaces: ['/summaries/{actorId}/{sessionId}'] },
{
type: 'EPISODIC',
namespaces: ['/episodes/{actorId}/{sessionId}'],
reflectionNamespaces: ['/episodes/{actorId}'],
},
],
};

const spec = minimalSpec({ memory: { name: 'my_memory' } });
const result = await mapHarnessSpecToCreateOptions({
...BASE_OPTIONS,
harnessSpec: spec,
deployedResources,
memorySpec,
});

expect(result.memory).toEqual({
agentCoreMemoryConfiguration: {
arn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123',
retrievalConfig: {
'/users/{actorId}/facts': {},
'/users/{actorId}/preferences': {},
'/summaries/{actorId}/{sessionId}': {},
'/episodes/{actorId}/{sessionId}': {},
'/episodes/{actorId}': {},
},
},
});
});

it('includes EPISODIC reflectionNamespaces in retrievalConfig', async () => {
const deployedResources: DeployedResourceState = {
memories: {
my_memory: {
memoryId: 'mem-123',
memoryArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123',
},
},
};
const memorySpec: Memory = {
name: 'my_memory',
eventExpiryDuration: 30,
strategies: [
{
type: 'EPISODIC',
namespaces: ['/episodes/{actorId}/{sessionId}'],
reflectionNamespaces: ['/episodes/{actorId}'],
},
],
};

const spec = minimalSpec({ memory: { name: 'my_memory' } });
const result = await mapHarnessSpecToCreateOptions({
...BASE_OPTIONS,
harnessSpec: spec,
deployedResources,
memorySpec,
});

expect(result.memory?.agentCoreMemoryConfiguration.retrievalConfig).toEqual({
'/episodes/{actorId}/{sessionId}': {},
'/episodes/{actorId}': {},
});
});

it('omits retrievalConfig when strategies have no namespaces or reflectionNamespaces', async () => {
const deployedResources: DeployedResourceState = {
memories: {
my_memory: {
memoryId: 'mem-123',
memoryArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123',
},
},
};
const memorySpec: Memory = {
name: 'my_memory',
eventExpiryDuration: 30,
strategies: [{ type: 'SEMANTIC' }, { type: 'SUMMARIZATION' }],
};

const spec = minimalSpec({ memory: { name: 'my_memory' } });
const result = await mapHarnessSpecToCreateOptions({
...BASE_OPTIONS,
harnessSpec: spec,
deployedResources,
memorySpec,
});

expect(result.memory?.agentCoreMemoryConfiguration.retrievalConfig).toBeUndefined();
});

it('includes EPISODIC reflectionNamespaces in retrievalConfig even without namespaces', async () => {
const deployedResources: DeployedResourceState = {
memories: {
my_memory: {
memoryId: 'mem-123',
memoryArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123',
},
},
};
const memorySpec: Memory = {
name: 'my_memory',
eventExpiryDuration: 30,
strategies: [
{ type: 'SEMANTIC' },
{
type: 'EPISODIC',
reflectionNamespaces: ['/episodes/{actorId}'],
},
],
};

const spec = minimalSpec({ memory: { name: 'my_memory' } });
const result = await mapHarnessSpecToCreateOptions({
...BASE_OPTIONS,
harnessSpec: spec,
deployedResources,
memorySpec,
});

expect(result.memory?.agentCoreMemoryConfiguration.retrievalConfig).toEqual({
'/episodes/{actorId}': {},
});
});

it('omits retrievalConfig when memorySpec not provided', async () => {
const deployedResources: DeployedResourceState = {
memories: {
my_memory: {
memoryId: 'mem-123',
memoryArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123',
},
},
};

const spec = minimalSpec({ memory: { name: 'my_memory' } });
const result = await mapHarnessSpecToCreateOptions({
...BASE_OPTIONS,
harnessSpec: spec,
deployedResources,
});

expect(result.memory?.agentCoreMemoryConfiguration.retrievalConfig).toBeUndefined();
});

it('includes both actorId and retrievalConfig when both are set', async () => {
const deployedResources: DeployedResourceState = {
memories: {
my_memory: {
memoryId: 'mem-123',
memoryArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123',
},
},
};
const memorySpec: Memory = {
name: 'my_memory',
eventExpiryDuration: 30,
strategies: [{ type: 'SEMANTIC', namespaces: ['/users/{actorId}/facts'] }],
};

const spec = minimalSpec({ memory: { name: 'my_memory', actorId: 'alice' } });
const result = await mapHarnessSpecToCreateOptions({
...BASE_OPTIONS,
harnessSpec: spec,
deployedResources,
memorySpec,
});

expect(result.memory).toEqual({
agentCoreMemoryConfiguration: {
arn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123',
actorId: 'alice',
retrievalConfig: {
'/users/{actorId}/facts': {},
},
},
});
});
});

// ── Truncation mapping ─────────────────────────────────────────────────
Expand Down
Loading
Loading