From 87f0d93286fae6070bd5250f4d613bdf382c021a Mon Sep 17 00:00:00 2001 From: Padma Komarina Date: Fri, 22 May 2026 19:53:38 -0400 Subject: [PATCH 1/3] fix(harness): populate retrievalConfig from memory strategy namespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without retrievalConfig in the CreateHarness payload, the harness runtime had no instruction to retrieve from any namespace at inference time — so long-term memory was written correctly but never read back. mapMemory now accepts the resolved Memory spec and derives retrievalConfig by collecting namespaces from all strategies. computeHarnessHash now includes the Memory spec so that changes to strategy namespaces in agentcore.json trigger a redeploy even when harness.json is unchanged. --- .../__tests__/harness-mapper.test.ts | 133 +++++++++++++++++- .../imperative/deployers/harness-deployer.ts | 17 ++- .../imperative/deployers/harness-mapper.ts | 27 +++- 3 files changed, 169 insertions(+), 8 deletions(-) diff --git a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts index a2ee5256b..f93542138 100644 --- a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts +++ b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts @@ -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'; @@ -406,6 +406,137 @@ 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}': {}, + }, + }, + }); + }); + + it('omits retrievalConfig when strategies have no explicit 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).toBeUndefined(); + }); + + 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 ───────────────────────────────────────────────── diff --git a/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts b/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts index ea5b31d5f..e0dfcdd31 100644 --- a/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts +++ b/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts @@ -5,7 +5,7 @@ * via the SigV4 API client. Harness role ARNs are resolved from CDK * stack outputs, and harness specs are read from disk (harness.json). */ -import type { HarnessDeployedState, HarnessSpec } from '../../../../../schema'; +import type { HarnessDeployedState, HarnessSpec, Memory } from '../../../../../schema'; import { HarnessSpecSchema } from '../../../../../schema'; import type { CreateHarnessResult, @@ -32,10 +32,18 @@ const READY_POLL_MAX_ATTEMPTS = 40; // 2 minutes max type HarnessDeployedStateMap = Record; -async function computeHarnessHash(harnessDir: string, harnessSpec: HarnessSpec, roleArn: string): Promise { +async function computeHarnessHash( + harnessDir: string, + harnessSpec: HarnessSpec, + roleArn: string, + memorySpec?: Memory +): Promise { const hash = createHash('sha256'); hash.update(JSON.stringify(harnessSpec)); hash.update(roleArn); + if (memorySpec) { + hash.update(JSON.stringify(memorySpec)); + } try { const promptContent = await readFile(join(harnessDir, 'system-prompt.md'), 'utf-8'); hash.update(promptContent); @@ -132,8 +140,9 @@ export class HarnessDeployer implements ImperativeDeployer m.name === harnessSpec.memory?.name); - const configHash = await computeHarnessHash(harnessDir, harnessSpec, executionRoleArn); + const configHash = await computeHarnessHash(harnessDir, harnessSpec, executionRoleArn, memorySpec); if (existingHarness?.configHash === configHash) { resultState[entry.name] = existingHarness; @@ -153,6 +162,7 @@ export class HarnessDeployer implements ImperativeDeployer; + /** The memory spec for the memory this harness references, used to derive retrievalConfig namespaces. */ + memorySpec?: Memory; } /** * Transform a HarnessSpec into CreateHarnessOptions for the control plane API. */ export async function mapHarnessSpecToCreateOptions(options: MapHarnessOptions): Promise { - const { harnessSpec, harnessDir, executionRoleArn, region, projectName, deployedResources, cdkOutputs } = options; + const { harnessSpec, harnessDir, executionRoleArn, region, projectName, deployedResources, cdkOutputs, memorySpec } = + options; const result: CreateHarnessOptions = { region, @@ -77,7 +80,7 @@ export async function mapHarnessSpecToCreateOptions(options: MapHarnessOptions): // Memory if (harnessSpec.memory) { - result.memory = mapMemory(harnessSpec.memory, deployedResources, cdkOutputs); + result.memory = mapMemory(harnessSpec.memory, deployedResources, cdkOutputs, memorySpec); } // Truncation @@ -268,7 +271,8 @@ function mapSkills(skills: string[]): HarnessSkill[] { function mapMemory( memory: NonNullable, deployedResources?: DeployedResourceState, - cdkOutputs?: Record + cdkOutputs?: Record, + memorySpec?: Memory ): HarnessMemoryConfiguration | undefined { let arn: string | undefined; @@ -295,14 +299,29 @@ function mapMemory( return undefined; } + // Build retrievalConfig from the memory's strategy namespaces so the harness + // runtime knows which namespaces to search at inference time. + const retrievalConfig = buildRetrievalConfig(memorySpec); + return { agentCoreMemoryConfiguration: { arn, ...(memory.actorId && { actorId: memory.actorId }), + ...(retrievalConfig && { retrievalConfig }), }, }; } +function buildRetrievalConfig( + memorySpec: Memory | undefined +): Record | undefined { + if (!memorySpec?.strategies?.length) return undefined; + + const namespaces = memorySpec.strategies.flatMap(s => s.namespaces ?? []); + + return namespaces.length > 0 ? Object.fromEntries(namespaces.map(ns => [ns, {}])) : undefined; +} + /** * Resolve memory ARN from CDK stack outputs. * The CDK construct exports memory ARNs with keys matching: From 628543580b166bc13467a0186ae1a41f7498496e Mon Sep 17 00:00:00 2001 From: Padma Komarina Date: Tue, 26 May 2026 05:56:39 -0400 Subject: [PATCH 2/3] fix(harness): include EPISODIC reflectionNamespaces in retrievalConfig buildRetrievalConfig now flattens reflectionNamespaces alongside namespaces for EPISODIC strategies, so cross-session reflection summaries stored at the parent namespace path are retrieved at inference time. Also adds a unit test asserting that mutating strategies[*].namespaces in agentcore.json produces a different configHash and triggers a harness update rather than being silently skipped. --- .../__tests__/harness-deployer.test.ts | 71 ++++++++++++++++++- .../__tests__/harness-mapper.test.ts | 68 +++++++++++++++++- .../imperative/deployers/harness-mapper.ts | 5 +- 3 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts index 4d5123852..648f36e76 100644 --- a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts +++ b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts @@ -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'; @@ -35,6 +35,7 @@ const CONFIG_ROOT = '/project/agentcore'; function createContext(overrides?: { harnesses?: AgentCoreProjectSpec['harnesses']; + memories?: Memory[]; deployedHarnesses?: DeployedState['targets'][string]['resources']; cdkOutputs?: Record; }): ImperativeDeployContext { @@ -43,7 +44,7 @@ function createContext(overrides?: { version: 1, managedBy: 'CDK' as const, runtimes: [], - memories: [], + memories: overrides?.memories ?? [], credentials: [], evaluators: [], onlineEvalConfigs: [], @@ -659,6 +660,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', () => { diff --git a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts index f93542138..e5835d5dc 100644 --- a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts +++ b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts @@ -447,12 +447,13 @@ describe('mapHarnessSpecToCreateOptions', () => { '/users/{actorId}/preferences': {}, '/summaries/{actorId}/{sessionId}': {}, '/episodes/{actorId}/{sessionId}': {}, + '/episodes/{actorId}': {}, }, }, }); }); - it('omits retrievalConfig when strategies have no explicit namespaces', async () => { + it('includes EPISODIC reflectionNamespaces in retrievalConfig', async () => { const deployedResources: DeployedResourceState = { memories: { my_memory: { @@ -465,9 +466,9 @@ describe('mapHarnessSpecToCreateOptions', () => { name: 'my_memory', eventExpiryDuration: 30, strategies: [ - { type: 'SEMANTIC' }, { type: 'EPISODIC', + namespaces: ['/episodes/{actorId}/{sessionId}'], reflectionNamespaces: ['/episodes/{actorId}'], }, ], @@ -481,9 +482,72 @@ describe('mapHarnessSpecToCreateOptions', () => { 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: { diff --git a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts index 92b0ed86d..273951a91 100644 --- a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts +++ b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts @@ -317,7 +317,10 @@ function buildRetrievalConfig( ): Record | undefined { if (!memorySpec?.strategies?.length) return undefined; - const namespaces = memorySpec.strategies.flatMap(s => s.namespaces ?? []); + const namespaces = memorySpec.strategies.flatMap(s => [ + ...(s.namespaces ?? []), + ...(s.type === 'EPISODIC' ? (s.reflectionNamespaces ?? []) : []), + ]); return namespaces.length > 0 ? Object.fromEntries(namespaces.map(ns => [ns, {}])) : undefined; } From ee91cc950a7f44bcc9898d93afb5a6db558ce7f7 Mon Sep 17 00:00:00 2001 From: Padma Komarina Date: Tue, 26 May 2026 12:34:00 -0400 Subject: [PATCH 3/3] fix(harness): resolve memorySpec by deployed ARN for arn-only memory refs When a harness references memory by ARN only (no name field), the previous lookup returned undefined, silently omitting retrievalConfig and excluding the memory from the configHash. resolveMemorySpec now walks deployedResources.memories to match by ARN and find the corresponding projectSpec memory, so name-less refs that point at a CLI-managed memory get the same treatment as name-based refs. HarnessMemoryRef is exported from the schema barrel so it can be used as the explicit parameter type on resolveMemorySpec. Adds unit tests for both the ARN-match path and the intentional undefined fallback for genuinely external memories. --- .../__tests__/harness-deployer.test.ts | 68 +++++++++++++++++++ .../imperative/deployers/harness-deployer.ts | 24 ++++++- src/schema/schemas/agentcore-project.ts | 8 ++- 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts index 648f36e76..e969f5206 100644 --- a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts +++ b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts @@ -536,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 }; diff --git a/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts b/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts index e0dfcdd31..1bd8b2baf 100644 --- a/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts +++ b/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts @@ -5,7 +5,13 @@ * via the SigV4 API client. Harness role ARNs are resolved from CDK * stack outputs, and harness specs are read from disk (harness.json). */ -import type { HarnessDeployedState, HarnessSpec, Memory } from '../../../../../schema'; +import type { + DeployedResourceState, + HarnessDeployedState, + HarnessMemoryRef, + HarnessSpec, + Memory, +} from '../../../../../schema'; import { HarnessSpecSchema } from '../../../../../schema'; import type { CreateHarnessResult, @@ -61,6 +67,20 @@ async function computeHarnessHash( return hash.digest('hex').slice(0, 16); } +function resolveMemorySpec( + memories: Memory[] | undefined, + memoryRef: HarnessMemoryRef | undefined, + deployedResources: DeployedResourceState | undefined +): Memory | undefined { + if (!memoryRef) return undefined; + if (memoryRef.name) return memories?.find(m => m.name === memoryRef.name); + if (memoryRef.arn && deployedResources?.memories) { + const entry = Object.entries(deployedResources.memories).find(([, v]) => v.memoryArn === memoryRef.arn); + if (entry) return memories?.find(m => m.name === entry[0]); + } + return undefined; +} + // ============================================================================ // Deployer // ============================================================================ @@ -140,7 +160,7 @@ export class HarnessDeployer implements ImperativeDeployer m.name === harnessSpec.memory?.name); + const memorySpec = resolveMemorySpec(projectSpec.memories, harnessSpec.memory, deployedResources); const configHash = await computeHarnessHash(harnessDir, harnessSpec, executionRoleArn, memorySpec); diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index 35a112fca..1260bd637 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -70,7 +70,13 @@ export type { ABTestMode, TargetRef, GatewayFilter, PerVariantOnlineEvaluationCo export { ABTestModeSchema, TargetRefSchema, GatewayFilterSchema } from './primitives/ab-test'; export type { HttpGatewayTarget } from './primitives/http-gateway'; export { HttpGatewayTargetSchema } from './primitives/http-gateway'; -export type { HarnessGatewayOutboundAuth, HarnessModel, HarnessSpec, HarnessModelProvider } from './primitives/harness'; +export type { + HarnessGatewayOutboundAuth, + HarnessMemoryRef, + HarnessModel, + HarnessSpec, + HarnessModelProvider, +} from './primitives/harness'; export { GatewayOAuthGrantTypeSchema, HarnessGatewayOutboundAuthSchema,